GatsbyJS https://jamesdflynn.com/ en GatsbyJS + Drupal: Create Content Type Landing Pages https://jamesdflynn.com/development/gatsbyjs-drupal-create-content-type-landing-pages <span class="field field--name-title field--type-string field--label-hidden">GatsbyJS + Drupal: Create Content Type Landing Pages</span> <span class="field field--name-uid field--type-entity-reference field--label-hidden"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">jflynn</span></span> <span class="field field--name-created field--type-created field--label-hidden">Sun, 12/01/2019 - 16:30</span> <div class="clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item"><p>At <a href="https://nedcamp.org/">NEDCamp</a> I had the pleasure of seeing many wonderful people and sessions, but, as you may know, my favorite track at most events is the "Hallway Track". If you're not familiar, the Hallway Track is the time in-between sessions where some real magic happens. You have a chance to talk with some of the most amazing minds in the world about topics you're passionate about. You can share knowledge, bounce ideas, have epiphanies, and realize that your current problems (code-wise) are things that other people have run into. One such conversation happened that inspired me to think outside the box to solve a problem.</p> <p>In my <a href="/development/gatsbyjs-drupal-create-custom-graphql-schema-empty-fields">last post</a> we went over how to create a related content section with references to entities that may have empty fields. Today, we're going to take that one step further and create landing pages for content types.</p> <p>In Drupal, we would ordinarily build these by attaching a view to a content type based on contextual filters or similar in order to get a collection of all content of that content type. Since we don't have Views in GatsbyJS, we need another solution. There are a couple of options out there, including the recently released <a href="https://www.drupal.org/project/jsonapi_cross_bundles">JSON:API Cross Bundles</a> module, compliments of Centarro and Matt Glaman. However, at the time of this writing there is an issue with the JSON:API Cross Bundles conflicting with <a href="https://www.drupal.org/project/jsonapi_extras">JSON:API Extras</a>. So, if you're relying on JSON:API Extras, you'll need another solution.</p> <h2>The problem:</h2> <p>Out of the box, JSON:API does not create a route to retrieve all nodes with any ease. However, there's no need to fear. This post is here!</p> <p>Now if you're not using JSON:API Extras, I strongly recommend looking into JSON:API Cross Bundles. It creates a route to all content and will simplify your life. If you are using JSON:API Extras, have I got something for you.</p> <p>Let's dive in.</p> <h2>Scenario:</h2> <p>You're building a decoupled Drupal site and you want to have a reusable template for landing pages that display all content of a content type. This is easy enough to do in Drupal using Views, but we lose Views when going decoupled so we need another way. How do we accomplish this in a decoupled application using GatsbyJS as the front-end?</p> <h2>Solution:</h2> <p>Strap in folks, this one gets a little bumpy.</p> <h3>Drupal side:</h3> <p>We need to do some work on both sides of the application for this to work. First, we will setup a content type in Drupal to use for our Content Landing Pages. This is kind of a choose your own adventure scenario, but one thing that you <strong><em>absolutely must have</em></strong> is an Entity Reference field that references the Config Entity: Content Type with a cardinality of 1. </p> <p>Select this field type:</p> <div alt="Dropdown select with Reference: Other selected" data-embed-button="media" data-entity-embed-display="media_image" data-entity-type="media" data-entity-uuid="5ae4689b-2121-4c63-82de-ea2d206fd13c" data-langcode="en" title="Select" class="embedded-entity"> <img src="/sites/default/files/Screen%20Shot%202019-12-01%20at%2011.33.23%20AM_0.png" alt="Dropdown select with Reference: Other selected" title="Select" typeof="foaf:Image" /></div> <p> </p> <p> And this is the entity type to reference:</p> <div alt="Dropdown select with Configuration: Content Type selected" data-embed-button="media" data-entity-embed-display="media_image" data-entity-type="media" data-entity-uuid="d7930f66-d6fc-4deb-bcb5-61240d79d76c" data-langcode="en" title="content type selected" class="embedded-entity"> <img src="/sites/default/files/Screen%20Shot%202019-12-01%20at%2011.36.44%20AM.png" alt="Dropdown select with Configuration: Content Type selected" title="content type selected" typeof="foaf:Image" /></div> <p> </p> <p>Now that we have our field created, select any content type that will require a content landing page as an available option.</p> <p>Whatever else you want to put on this page is up to you. Go wild. Want a hero image? Add a hero image. Want a dancing baby gif? That's your choice and I respect it. Once you've finished making the <strong>greatest landing page content type ever™ </strong>we can move on to the fun part in GatsbyJS.</p> <h3>GatsbyJS side:</h3> <p>JSON:API gives us something that can help us out a bit. It goes by the name <code>allNodeTypeNodeType</code> and it will let us workaround the lack of a base all-content route. If we explore this in GraphiQL we'll see that we can drill down a bit and get exactly what we need.</p> <pre> <code class="language-javascript">{ allNodeTypeNodeType { nodes { relationships { node__article node__landing_page node__basic_page } } } }</code></pre> <p><strong>NOTE: </strong>I didn't drill down too far, but all fields are available here.</p> <p>Let's first create our landing page template in Gatsby.</p> <p>First, let's just create a simple, empty component for our Landing Page with a basic graphql query.</p> <pre> <code class="language-javascript">// src/components/templates/LandingPage/index.js import React from 'react' import { graphql } from 'gatsby' function LandingPage({ data }) { return ( &lt;div&gt; &lt;/div&gt; ) } export default LandingPage export const query = graphql` query($LandingPageID: String!){ nodeLandingPage(id: { eq: $LandingPageID }) { id title } } `</code></pre> <p>Nothing too fancy here, right? Just a template that we can use to build our pages out without Gatsby yelling at us on build.</p> <p>Next, we're going to add this template to <code>gatsby-node.js</code> so that we create our pages dynamically.</p> <pre> <code class="language-javascript">// gatsby-node.js exports.createPages = async ({ graphql, actions }) =&gt; { const { createPage } = actions const LandingPageTemplate = require.resolve(`./src/components/templates/LandingPage/index.js`) const LandingPage = await graphql(`{ allNodeLandingPage { nodes { id path { alias } } } } `) LandingPage.data.allNodeLandingPage.nodes.map(node =&gt; { createPage({ path: node.path.alias component: LandingPageTemplate, context: { LandingPageID: node.id, } }) }) }</code></pre> <p>This is pretty straightforward so far, right? Let's think about what we're going to need in order for this to work the way we want it to and pull all of a single content type into our landing page.</p> <p>We're going to need:</p> <ul><li>The content type we want to build a landing page for.</li> <li>A query to fetch all content a content type.</li> <li>The landing page template with logic to display the content type.</li> <li>Probably some other things, but we'll sort that out along the way. We're in this together, remember?</li> </ul><p>How are we going to get these things? Let's go down the list.</p> <p><strong>The content type we want to build a landing page from:</strong></p> <p>We have this from our Drupal side. Remember, we created the Content Type field on our Landing Page content type? This can be placed in our <code>gatsby-node.js</code> and passed to our query via the <code>context</code> option.</p> <p>Let's add it in.  First we need to update our graphql query to pull it in:</p> <pre> <code class="language-javascript">// gatsby-node.js const LandingPage = await graphql(`{ allNodeLandingPage { nodes { id relationships { // &lt;---- add from this line field_content_type { drupal_internal__type name } } // &lt;---- to this line } } } `)</code></pre> <p>What we're doing here is looking at GraphiQL and exploring our data to see what we have available. If we drill down into <code>allNodeLandingPage.nodes</code> we can see that in <code>relationships</code> we have <code>field_content_type</code> with some useful things. Specifically, our <code>drupal_internal__type</code> and <code>name</code> values. Also, notice that we removed <code>nodes.path.alias</code> from the query.</p> <p>By adding these to our query we can now pass the info through to our created pages. We're going to do a bit of data manipulation here to create our paths dynamically as well. I follow the convention that a landing page's path should reflect the content type that it's a landing page for. So, if we were making a landing page for "Articles" the path would be <code>path-to-my.site/articles</code> and articles would have that as a base path to <code>path-to-my.site/articles/my-awesome-article</code>. However, you can follow whatever convention you see fit.</p> <p>To do this, we're going to manipulate the name from the content type into a URL-friendly string by using the JavaScript <code>.replace()</code> function and then pass that to the <code>path</code> option. Since we also want to query for the content type on our landing page, we're going to pass the <code>drupal_internal__type</code> through the <code>context</code> option.</p> <p>Let's do that:</p> <pre> <code class="language-javascript">// gatsby-node.js LandingPage.data.allNodeLandingPage.nodes.map(node =&gt; { const pathName = node.relationships.field_content_type.name.toLowerCase().replace(/ /g, '-') // &lt;---- New line createPage({ path: pathName, // &lt;--- Changed line component: LandingPageTemplate, context: { LandingPageID: node.id, ContentType: node.relationships.field_content_type.drupal_internal__type, // &lt;---- New line } }) })</code></pre> <p>What does the the context option do? It passes data to our component as props. GraphQL already pulls the context data for the queries, which you can see in any query that has a variable for the filter. Usually this is the content ID so that it can build a page for a specific piece of content from Drupal, but we can leverage this to add more variables and more filtering however we see fit.</p> <p>Our next step is going to be to actually USE this additional info to do something amazing with.</p> <p><strong>A query to fetch all content a content type:</strong></p> <p>Let's look back at our <code>src/components/templates/LandingPage/index.js</code> and see what we need to query. We know we want to get all nodes of a certain content type, and we know that we want to reuse this template for any landing pages with content listing. Since we've established that allNodeTypeNodeType gives us access to all content on available to Gatsby, let's query on that.</p> <pre> <code class="language-javascript">// src/components/templates/LandingPage/index.js export const query = graphql` query($LandingPageID: String!, $ContentType: String!){ nodeLandingPage(id: { eq: $LandingPageID }) { id title } allNodeTypeNodeType(filter: { drupal_internal__type: { eq: $ContentType }}) { // &lt;---- New section nodes { relationships { node__article { id title path { alias } } node__page { id title path { alias } } } } } } `</code></pre> <p>What we're doing here is using that variable we passed via the context option in gatsby-node.js and filtering to only return the content type we're wanting to see. One 'gotcha' here is that this query will also return the landing page that references the content type. However, if you're not creating a landing page of landing pages then you should be alright.</p> <p>Since we're only creating landing pages for two content types, this is fine, although we're not getting a lot back. Most projects that I've worked on have had some kind of "teaser" display for these kinds of pages. I'm not going to cover the specifics of creating a teaser template here, but the TL;DR is: start with your full display and take out everything but what you want on the teaser. For this post, we're going to create the list of links using the titles.</p> <p>Now, if the content types that we're creating landing pages for don't have any content, then you're going to have a bad time. In this case, go back to my previous post about empty entity reference fields and see if you can use that to create some default fields and prevent errors or just create some content of the missing type.</p> <p>Next, let's flesh out our landing page template a bit.</p> <p><strong>The landing page template with logic to display the content type:</strong></p> <p>So far, our template, minus the query, is pretty empty and not doing a lot. Let's add in the title of this landing page.</p> <pre> <code class="language-javascript">// src/components/templates/LandingPage/index.js function LandingPage({ data }) { const landingPage = data.nodeLandingPage return ( &lt;div&gt; &lt;h1&gt;{landingPage.title}&lt;/h1&gt; &lt;div className='content-list'&gt;&lt;/div&gt; &lt;/div&gt; ) }</code></pre> <p>I like to clean up the variables a bit and rename <code>data.nodeLandingPage</code> to <code>landingPage</code>. It's a bit cleaner to me, but do what you want.</p> <p>Alright, we have the title of this content, but what about the list of content we want to show on this page? Well, we're going to need to do some logic for that. First off, we need to know which content type we're looking for. Second, we need a way find it. Third, we need to clean this data into something usable. Finally, we need to display it.</p> <p>We could just display everything returned from our <code>allNodeTypeNodeType</code> query, but there would be a lot of nulls and issues parsing the arrays. Here's an example of what that query returns before we massage the data, using the Drupal internal type <code>article</code>:</p> <pre> <code class="language-json">{ "data": { "allNodeTypeNodeType": { "nodes": [ { "drupal_internal__type": "article", "relationships": { "node__article": [ { "id": "0e68ac03-8ff2-54c1-9747-3082a565bba6", "title": "Article Template", "path": { "alias": "/article/article-template" } } ], "node__basic_page": null } } ] } } }</code></pre> <p>Now, to get the content this way we could do some complex mapping and sorting and filtering, but I tried that and it wasn't fun. Fortunately, Gatsby is here to rescue us and make life easier. Our context option gets passed into our page component as props. If you're unfamiliar with the concept of Props in React, and therefore Gatsby, props are properties that are passed into components. The line </p> <pre> <code>function LandingPage({ data }) {</code></pre> <p>could be rewritten as</p> <pre> <code>function LandingPage(props) { const data = props.data</code></pre> <p>but we're using a concept called <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring">Destructuring</a> to only pass in the prop that we need. This allows us to create variables from object keys without having to take the extra steps. Our page component props object also contains the key <a href="https://www.gatsbyjs.org/docs/actions/#createPage">pageContext</a> which is where anything in the context option gets stored to give the page template access to.</p> <p>Let's bring that in:</p> <pre> <code class="language-javascript">// src/components/templates/LandingPage/index.js function LandingPage({ data, pageContext }) { const landingPage = data.nodeLandingPage const nodeType = data.allNodeTypeNodeType const contentType = 'node__' + pageContext.ContentType</code></pre> <p>Since we set our <code>ContentType</code> in <code>gatsby-node.js</code> we're able to use that here. Note that we're concatenating the string <code>node__</code> with our <code>pageContext.ContentType</code>. We're doing this because everything in Gatsby is a node, including content types. This allows us to do the next steps.</p> <p>Next, we want to clear out all of the non-content type data from the <code>allNodeTypeNodeType</code> query. This is what it looks like if we were to <code>console.log(nodeType.nodes)</code>:</p> <pre> <code>Array(1) 0: relationships: node__article: Array(1) 0: {id: "0e68ac03-8ff2-54c1-9747-3082a565bba6", title: "Article Template", path: {…}, …} length: 1 __proto__: Array(0) node__page: null</code></pre> <p>We only want the <code>node__article</code> array, so how do we get that? Well, we need to use <code>.map()</code> and a concept called <a href="https://www.sitepoint.com/currying-in-functional-javascript/">currying</a>. This is essentially creating a function that allows us to use a variable from outside of the <code>.map()</code> scope inside of the <code>.map()</code> callback. It allows us to break down a function into more functions so that we have more control over it, which is what we need here.</p> <pre> <code class="language-javascript">// src/components/templates/LandingPage/index.js function LandingPage({ data, pageContext }) { const landingPage = data.nodeLandingPage const nodeType = data.allNodeTypeNodeType const contentType = 'node__' + pageContext.ContentType const getContentArray = (contentType) =&gt; { // &lt;---- Curry function, but not as delicious return (node) =&gt; (node.relationships[contentType]) } const contentArray = nodeType.nodes.map(getContentArray(contentType))</code></pre> <p>We created our curry function that takes our <code>contentType</code> as an argument. From within there, it completes the mapping and returns our desired array... almost.</p> <p>Here's what we get back if we <code>console.log(contentArray)</code>:</p> <pre> <code>[Array(1)] 0: Array(1) 0: {id: "0e68ac03-8ff2-54c1-9747-3082a565bba6", id: 1, title: "Article Template", …} length: 1 __proto__: Array(0) length: 1 __proto__: Array(0) </code></pre> <p>We're almost there, but now we have an array of our content within another array. If only there were a function to help us out here...</p> <p>Just kidding, there is! For this, we're going to use <code>.flat()</code>. The <code>.flat()</code> function flattens out a nested array into a single level. However, there's a gotcha with it, as mentioned in this <a href="https://stackoverflow.com/questions/56593033/array-prototype-flat-creates-an-error-when-building-gatsby-site">Stack/Overflow question</a>. We can get around this by using the array-flat-polyfill polyfill.</p> <p>Add gatsby-plugin-polyfill-io to your project by installing with yarn or npm</p> <pre> <code>npm install array-flat-polyfill // or yarn add array-flat-polyfill</code></pre> <p>and in your component file add the following within</p> <pre> <code>import 'array-flat-polyfill'</code></pre> <p>So, let's flatten that array!</p> <pre> <code class="language-javascript">function LandingPage({ data, pageContext }) { const landingPage = data.nodeLandingPage const nodeType = data.allNodeTypeNodeType const contentType = 'node__' + pageContext.ContentType const getContentArray = (contentType) =&gt; { return (node) =&gt; (node.relationships[contentType]) } const contentArray = nodeType.nodes.map(getContentArray(contentType)) const contentArrayFlat = contentArray.flat()</code></pre> <p>And the resulting <code>console.log(contentArrayFlat)</code>:</p> <pre> <code>0: id: "0e68ac03-8ff2-54c1-9747-3082a565bba6" path: {alias: "/article/article-template"} title: "Article Template" length: 1 __proto__: Array(0)</code></pre> <p>Now we've got exactly what we wanted! The final step is to put this to work. We'll do that by creating a list of titles that link to the content. Your finished component should look like:</p> <pre> <code class="language-javascript">// src/components/templates/LandingPage/index.js import React from 'react' import { graphql, Link } from 'gatsby' // &lt;--- added 'Link' here to use the link component import 'array-flat-polyfill' function LandingPage({ data, pageContext }) { const landingPage = data.nodeLandingPage const nodeType = data.allNodeTypeNodeType const contentType = 'node__' + pageContext.ContentType const getContentArray = (contentType) =&gt; { return (node) =&gt; (node.relationships[contentType]) } const contentArray = nodeType.nodes.map(getContentArray(contentType)) const contentArrayFlat = contentArray.flat() return ( &lt;div&gt; &lt;h1&gt;{landingPage.title}&lt;/h1&gt; &lt;div className='content-list'&gt; &lt;ul&gt; // One-liner to create the list of items. {contentArrayFlat.map((item, i) =&gt; &lt;li key={i}&gt;&lt;Link to={item.path.alias}&gt;{item.title}&lt;/Link&gt;&lt;/li&gt;)} &lt;/ul&gt; &lt;/div&gt; &lt;/div&gt; ) } export default LandingPage export const query = graphql` query($LandingPageID: String!, $ContentType: String!){ nodeLandingPage(id: { eq: $LandingPageID }) { id title } allNodeTypeNodeType(filter: { drupal_internal__type: { eq: $ContentType }}) { nodes { relationships { node__article { id title path { alias } } node__page { id title path { alias } } } } } } `</code></pre> <p>And that's all there is to it. Hopefully you find this useful and it helps speed up your development with Gatsby a little bit.  If I missed anything on here, please don't hesitate to let me know in the comments. Always feel free to reach out to me on Twitter or Slack or any way you want to.</p> <p>Credit where credit is due: Shane Thomas (AKA <a href="https://twitter.com/codekarate">@codekarate</a>) and Brian Perry (AKA <a href="https://twitter.com/bricomedy">@bricomedy</a>) helped me work through this issue at NEDCamp.</p> <h2>Patron thanks:</h2> <p>Thank you to my Patreon Supporters</p> <ul><li>David Needham</li> <li>Tara King</li> <li>Lullabot</li> </ul><p>For helping make this post. If you'd like to help support me on Patreon, check out my page <a href="https://www.patreon.com/jddoesthings">https://www.patreon.com/jddoesthings</a></p> </div> <div class="field field--name-field-category field--type-entity-reference field--label-above"> <div class="field__label">Category</div> <div class="field__item"><a href="/development" hreflang="en">Development</a></div> </div> <div class="field field--name-field-tags field--type-entity-reference field--label-above"> <div class="field__label">Tags</div> <div class="field__items"> <div class="field__item"><a href="/taxonomy/term/12" hreflang="en">Drupal Planet</a></div> <div class="field__item"><a href="/taxonomy/term/21" hreflang="en">GatsbyJS</a></div> <div class="field__item"><a href="/taxonomy/term/14" hreflang="en">react</a></div> <div class="field__item"><a href="/taxonomy/term/11" hreflang="en">Drupal</a></div> </div> </div> <div class="field field--name-field-comments field--type-disqus-comment field--label-visually_hidden"> <div class="field__label visually-hidden">Comments</div> <div class="field__item"><drupal-render-placeholder callback="Drupal\disqus\Element\Disqus::displayDisqusComments" arguments="0=GatsbyJS%20%2B%20Drupal%3A%20Create%20Content%20Type%20Landing%20Pages&amp;1=https%3A//jamesdflynn.com/development/gatsbyjs-drupal-create-content-type-landing-pages&amp;2=node/26" token="h4NQmV_u2BZZnR5IlizYepQ0xjfGCxWDuwZZqh1Y-y4"></drupal-render-placeholder></div> </div> <div class="field field--name-field-header-image-entity field--type-entity-reference field--label-hidden field__item"><article class="media media--type-image media--view-mode-embedded"> <div class="field field--name-image field--type-image field--label-hidden field__item"> <img src="/sites/default/files/negative-space-compiled-code-screen.jpg" width="1200" height="800" alt="Generic &quot;Look at my screen full of code&quot; screen" loading="lazy" typeof="foaf:Image" /> </div> </article> </div> Sun, 01 Dec 2019 22:30:00 +0000 jflynn 26 at https://jamesdflynn.com GatsbyJS + Drupal: Create Custom GraphQL Schema for Empty Fields https://jamesdflynn.com/development/gatsbyjs-drupal-create-custom-graphql-schema-empty-fields <span class="field field--name-title field--type-string field--label-hidden">GatsbyJS + Drupal: Create Custom GraphQL Schema for Empty Fields</span> <span class="field field--name-uid field--type-entity-reference field--label-hidden"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">jflynn</span></span> <span class="field field--name-created field--type-created field--label-hidden">Mon, 11/18/2019 - 14:40</span> <div class="clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item"><h2>Stop me if you've heard this one...</h2> <p>A developer is trying to connect Drupal to Gatsby. Drupal says, "I've got a Related Content field that allows SEVEN different entity types to be referenced, but only THREE slots for content. What do you think?"</p> <p>Developer says, "Sure, no problem, Drupal!"</p> <p>Gatsby jumps in and says, "You better make sure that all seven of those entity types are in those three slots."</p> <p>Developer and Drupal look at each other in confusion. Developer says, "Gatsby, that's not GREAT"</p> <p><em>** Laughs go here **</em></p> <div alt="That's the joke meme" data-embed-button="media" data-entity-embed-display="media_image" data-entity-embed-display-settings="medium" data-entity-type="media" data-entity-uuid="133fbad1-73ba-4652-a245-437da93b9ba2" data-langcode="en" title="Simpsons" class="embedded-entity"> <img src="/sites/default/files/styles/medium/public/joke.jpg?itok=4QDapDCs" alt="That's the joke meme" title="Simpsons" typeof="foaf:Image" class="image-style-medium" /></div> <h2>What happened?</h2> <p>It turns out the way that Gatsby builds what it calls "nodes" is by querying the data available. This usually works perfectly, but there are occasions where all the data might not be present, so Gatsby doesn't build the nodes, or it builds the nodes, but doesn't create all of the fields.  Any optional field can fall into this if there is no content in that field on a Drupal entity, and if you're building templates for your decoupled site, then you might just want some content in that field.</p> <p><strong>NOTE: </strong>A Drupal "node" and a GraphQL "node" are not the same thing. This may take some time to sink in. It definitely did for me! For the remainder of this post I will refer to GraphQL nodes as "nodes" and Drupal nodes as "entities". Everybody got that? Good, moving on.</p> <p>Because the nodes with your hypothetical content have not yet been created, or the fields on your entity don't have anything in them, Gatsby just passes right over them and acts like they don't even exist.</p> <p>Commonly, you'll see something like this pop up during <code>gatsby build</code> or <code>gatsby develop</code>:</p> <pre> <code class="language-bash">Generating development JavaScript bundle failed /var/www/gatsby/src/components/templates/homepage/index.js 56:9 error Cannot query field "field_background_media" on type "node__homepageRelationships".</code></pre> <p>This can be frustrating. And when I say "frustrating" I mean that it's table-flipping, head slamming into desk, begging for help from strangers on the street frustrating. The quick fix for this one is simple: ADD SOMETHING TO THE FIELD. In fact, you don't even have to add it to every instance of that field. You can get away with only putting some dummy content in a single entity and you'll have access to that field in Gatsby.</p> <p>You could also make it a required field so that there MUST be something in it or else the author can't save. This is valid, but in the event that your project's powers-that-be decide that it should be optional, you may need another alternative. This is where GraphQL schema customization comes in.</p> <h2>GraphQL schema and Gatsby</h2> <p>First, a little primer on GraphQL schema. Outside of Gatsby, a system using GraphQL usually needs to define the schema for GraphQL nodes. Gatsby takes this out of the developer's hands and builds the schema dynamically based on the content available to it from its source, in this case <code>gatsby-source-drupal</code>. However, empty data in the source results in no schema in Gatsby.</p> <p>Let's look at the fix for the optional image above and why it works:</p> <p>In <code>gatsby-node.js</code> we build our schema through <code>createPages</code>, <code>createPage</code>, and GraphQL queries. This is also where we're going to place our code to help GraphQL along a little bit. This will live in a function called <code>createSchemaCustomization</code> that we can use to customize our GraphQL schema to prevent empty field errors from ruining our day.</p> <p>The Gatsby docs outline this in the <a href="https://www.gatsbyjs.org/docs/schema-customization/">Schema Customization section</a>, but it didn't really click there for me because the examples are based on sourcing from Markdown as opposed to Drupal or another CMS. Things finally clicked a little when I found <a href="https://github.com/gatsbyjs/gatsby/issues/16809">this issue</a> on Github that showed how to work around a similar problem when sourcing from WordPress.</p> <h2>Three scenarios and three solutions</h2> <p><strong>Scenario 1: Empty field</strong></p> <p>This is by far the simplest scenario, but that doesn't make it simple. If you have a basic text field on a Drupal entity that does not yet have any content or at some point may not have any content, you don't want your queries to break your site. You need to tell GraphQL that the field exists and it needs to create the matching schema for it.</p> <p>Here's how it's done:</p> <pre> <code class="language-javascript">exports.createSchemaCustomization = ({ actions }) =&gt; { const { createTypes } = actions const typeDefs = ` type node__homepage implements Node { field_optional_text: String ` createTypes(typeDefs) }</code></pre> <p>Now, to walk through it a bit. We're using <code>createSchemaCustomization</code> here and passing in <code>actions</code>. The <code>actions</code> variable is an object that contains several functions so we destructure it to only use the one we need here: <code>createTypes</code>.</p> <p>We define our types in a GraphQL query string that we lovingly call <code>typeDefs.</code> The first line of the query tells which type we're going to be modifying, in this case <code>node__homepage</code>, and then what interface we're implementing, in this case <code>Node</code>. </p> <p><strong>NOTE: </strong>If you're looking at GraphiQL to get your field and node names it may be a bit misleading. For example, in GraphiQL and in my other queries <code>node__homepage</code> is called as <code>nodeHomepage</code>. You can find out what the type name is by running a query in GraphiQL that looks like:</p> <pre> <code>query MyQuery { nodeHomepage { internal { type } } }</code></pre> <p>or by exploring your definitions in GraphiQL.</p> <p>Next, we add the line <code>field_optional_text: String</code> which is where we define our schema. This is telling GraphQL that this field should exist and it should be of the type <code>String</code>. If there are additional fields, Gatsby and GraphQL are smart enough to infer them so you don't have to redefine every other field on your node.</p> <p>Finally, we pass our customizations into createTypes and restart our development server. No more errors!</p> <p><strong>Scenario 2: Entity reference field</strong></p> <p>Suppose we have a field for media that may not always have content. We still want our queries to work in Gatsby, but if there's only one Drupal entity of this content type and it happens to not have an image attached, then our queries are going to break, and so will our hearts. This requires a foreign key relationship because the entity itself is a node to GraphQL. </p> <p>Here's how it's done:</p> <pre> <code class="language-javascript">exports.createSchemaCustomization = ({ actions }) =&gt; { const { createTypes } = actions const typeDefs = ` type node__homepage implements Node { relationships: node__homepageRelationships } type node__homepageRelationships { field_background_media: media__image @link(from: "field_background_media___NODE") } ` createTypes(typeDefs)</code></pre> <p>Looks a little similar to the previous scenario, but there's a bit more to it. Let's look at the changes.</p> <p>Instead of defining our custom schema under the <code>type node__homepage implements Node {</code> line, we reference our <code>relationships</code> field which is where all referenced nodes live. In this case, we tell GraphQL that the <code>relationships</code> field is of type <code>node__homepageRelationships</code>. This is another thing you can find using GraphiQL. </p> <p>The next section further defines our relationships field. We declare that <code>node__homepageRelationships</code> should have the field <code>field_background_media</code> of type <code>media__image</code>, but what's that next part? That's where our foreign key link is happening. Since <code>field_background_image</code> is an entity reference, it becomes a node. This line links the field to the node created from the field. If you don't include the <code>@link(from: "field_background_media___NODE)</code> in your query you'll get a deprecation notice:</p> <pre> <code class="language-bash">warn Deprecation warning - adding inferred resolver for field node__homepageRelationships.field_background_media. In Gatsby v3, only fields with an explicit directive/extension will get a resolver.</code></pre> <p><em>Edit: This post was edited to reflect the deprecation notice instead of indicating that </em><code>@link</code><em> is optional.</em></p> <p><strong>Scenario 3: Related content field with multiple content types</strong></p> <p>This is the one that had me crying. I mean scream-crying. This is the widowmaker, the noisy killer, the hill to die upon. At least it WAS, but I am triumphant!</p> <p>Like my horrible joke from the beginning of this post, I have a related content field that is supposed to allow pretty much any content type from Drupal, but only has three slots. I need to have queries available for each content type, but if one doesn't exist on the field, then GraphQL makes a fuss. I can't say for sure, but I've got a feeling that it also subtly insulted my upbringing somewhere behind the scenes.</p> <p>Now, I'm here to save you some time and potentially broken keyboards and/or monitors.</p> <p>Here's how it's done:</p> <pre> <code class="language-javascript">exports.createSchemaCustomization = ({ actions }) =&gt; { const { createTypes } = actions const typeDefs = ` union relatedContentUnion = node__article | node__basic_page | node__blog_post | node__cat_food_reviews | node__modern_art_showcase | node__simpsons_references type node__homepage implements Node { relationships: node__homepageRelationships } type node__homepageRelationships { field_related_content: [relatedContentUnion] @link(from: "field_related_content___NODE") } ` createTypes(typeDefs) }</code></pre> <p>The biggest changes here are the union and the brackets around the type. From the GraphQL docs:</p> <blockquote class="pullquote"> <p>Union types are very similar to interfaces, but they don't get to specify any common fields between the types.</p> </blockquote> <p>This means that any type in the union can be returned and they don't need to have the same fields. Sounds like exactly what we need, right?</p> <p>So we define our union of our six content types and create the relationship using a foreign key link. We use the node created from our field, <code>field_related_content___NODE</code> as our foreign reference, and we get results. However, the brackets around the union are there, and they must do something. In fact they do. They indicate that what will be returned is an array of content in this union, meaning that there will be multiple values instead of one single value returned.</p> <h2><strong>Summary</strong></h2> <p>This seriously took me much longer than it should have to figure out. Hopefully, you don't suffer my same fate and find this post useful. One thing to note that almost destroyed me is that there are THREE, count them 1, 2, 3, underscores between the field name and <code>NODE</code> in the link. <code>_ _ _ NODE</code>. I only put two when I was fighting this and it cost me more time than I'm willing to admit on a public forum.</p> <p>Special thanks to Shane Thomas (AKA <a href="http://codekarate.com/">Codekarate</a>) for helping me sort through this and being able to count to 3 better than me.</p> </div> <div class="field field--name-field-category field--type-entity-reference field--label-above"> <div class="field__label">Category</div> <div class="field__item"><a href="/development" hreflang="en">Development</a></div> </div> <div class="field field--name-field-tags field--type-entity-reference field--label-above"> <div class="field__label">Tags</div> <div class="field__items"> <div class="field__item"><a href="/taxonomy/term/12" hreflang="en">Drupal Planet</a></div> <div class="field__item"><a href="/taxonomy/term/21" hreflang="en">GatsbyJS</a></div> <div class="field__item"><a href="/taxonomy/term/14" hreflang="en">react</a></div> <div class="field__item"><a href="/taxonomy/term/11" hreflang="en">Drupal</a></div> </div> </div> <div class="field field--name-field-comments field--type-disqus-comment field--label-visually_hidden"> <div class="field__label visually-hidden">Comments</div> <div class="field__item"><drupal-render-placeholder callback="Drupal\disqus\Element\Disqus::displayDisqusComments" arguments="0=GatsbyJS%20%2B%20Drupal%3A%20Create%20Custom%20GraphQL%20Schema%20for%20Empty%20Fields&amp;1=https%3A//jamesdflynn.com/development/gatsbyjs-drupal-create-custom-graphql-schema-empty-fields&amp;2=node/25" token="3U0OmjwqUk4j8czFhv_IrIkL3AGHqhjP-aNp-01PMWY"></drupal-render-placeholder></div> </div> <div class="field field--name-field-header-image-entity field--type-entity-reference field--label-hidden field__item"><article class="media media--type-image media--view-mode-embedded"> <div class="field field--name-image field--type-image field--label-hidden field__item"> <img src="/sites/default/files/negative-space-compiled-code-screen.jpg" width="1200" height="800" alt="Generic &quot;Look at my screen full of code&quot; screen" loading="lazy" typeof="foaf:Image" /> </div> </article> </div> Mon, 18 Nov 2019 20:40:00 +0000 jflynn 25 at https://jamesdflynn.com Using Drupal Blocks in a Decoupled GatsbyJS Application https://jamesdflynn.com/development/using-drupal-blocks-decoupled-gatsbyjs-application <span class="field field--name-title field--type-string field--label-hidden">Using Drupal Blocks in a Decoupled GatsbyJS Application</span> <span class="field field--name-uid field--type-entity-reference field--label-hidden"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">jflynn</span></span> <span class="field field--name-created field--type-created field--label-hidden">Sun, 11/03/2019 - 12:40</span> <div class="clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item"><p>One of the most useful components of Drupal is the <a href="https://www.drupal.org/docs/8/core/modules/block">Block system</a>. This system allows for pieces of content to be created and reused throughout a site in various regions of the page structure. You can also select which bundles or content types this piece of content should appear on, making it so that a blog post gets a certain Call to Action section while an article gets something similar, but different.</p> <p>When we use GatsbyJS for a decoupled front-end solution with a Drupal back-end, we lose out on quite a bit, if not everything, that the theme layer of Drupal provides. This means that Drupal regions mean nothing to Gatsby, nor does block placement. In fact, any functionality that would go into a <code>.theme</code> file is no longer available on the Drupal side.</p> <p>Before I get into the "how" of using Drupal blocks in Gatsby, I want to cover a little bit of the "why".</p> <h2>Why Use Drupal Blocks for Content in a Gatsby Front End?</h2> <p>The main advantage of blocks in Drupal is that the content is created in one place, but can be placed in several spots. I have worked with solutions that use Paragraphs (from the Paragraphs contrib module) for similar functionality, but the problem remains where the content needs to be recreated on every new parent entity. For example, I can create a Call to Action (CTA) Paragraph and place fields that reference them on every content type, but the Paragraphs themselves remain separate. A Paragraph is more like a collection of fields to be filled out than a reusable entity, and that's okay.</p> <p>In contrast, a Block can be created in one place, usually using the Block UI, and the content of the Block remains the same no matter where it is placed. A CTA block on a blog post will have identical content to the CTA block on an article. This makes content changes extremely fast compared to having to update every article and blog post if the wording or link need to change.</p> <p>It is entirely possible to create these types of entities in Gatsby by defining a reusable component, however the editing experience doesn't really pan out. It may require a developer to go in and edit a React component, adding a middle-person to the process. Using reusable Drupal blocks can save time and budget when set up appropriately.</p> <h2>How to Use Drupal Blocks for Content in a Gatsby Front End</h2> <p>This section is going to make a few assumptions.</p> <ol><li>You have a basic understanding of Gatsby and React components.</li> <li>You understand the Drupal theme layer.</li> <li>You have a basic understanding of the Drupal block system.</li> <li>Your decoupled application is already setup and pulling content from Drupal (not necessary, but it helps if you can try it out)</li> <li>You have the JSON:API module enabled on Drupal</li> <li>You're sourcing content from Drupal.</li> </ol><p>If you're having problems with any of these, feel free to reach out to me on Drupal Slack, I go by Dorf there.</p> <p>Now the "how".  We're going to start by creating a Custom Block Type. Let's keep going with the CTA theme and call it "CTA Block". We do this by logging into our site as someone with permissions to access the Block UI and going to <code>admin/structure/block/block-content/types.</code> Once there, select "Add custom block type".</p> <p>Let's label this CTA Block and begin creation. After we create it, we need to add some fields, so let's add the following fields:</p> <ul><li><strong>CTA Heading</strong> <ul><li>A plain text field, max 255 chars</li> </ul></li> <li><strong>CTA Link</strong> <ul><li>A Link field using URL and label, internal and external links, Label required.</li> </ul></li> <li><strong>CTA Content Types</strong> <ul><li>This is the field that will make all the difference. Create this field as Reference: Other... to begin with.</li> <li>Label it CTA Content Type.</li> <li>Under "Type of entity to reference" choose "Content type" from the select list, under Configuration.</li> <li>Set unlimited cardinality.</li> <li>Go through the remaining screens to create this field. Once you're back on the field list, select "Manage form display"</li> <li>From here, we're going to change the widget from Autocomplete to Check boxes/radio buttons.</li> <li>Save your changes</li> </ul></li> </ul><p>Now we're going to create a new block using this Custom Block Type. When we create the block under <code>block/add/cta_block</code> the form should look something like this:</p> <div alt="Screenshot of Block form" data-embed-button="media" data-entity-embed-display="media_image" data-entity-embed-display-settings="crop_freeform" data-entity-type="media" data-entity-uuid="016cec0b-963a-4042-a527-566d7f30d5d5" data-langcode="en" title="Block Form" class="embedded-entity"> <img src="/sites/default/files/styles/crop_freeform/public/Screen%20Shot%202019-11-03%20at%205.48.19%20PM_0.png?itok=ku5y9vEF" alt="Screenshot of Block form" title="Block Form" typeof="foaf:Image" class="image-style-crop-freeform" /></div> <p>Now, add whatever text you want to the fields, but only select a single content type in the CTA Content Type field.  Save the block and spin up your Gatsby dev environment. We're going to switch over there for a bit.</p> <p>Let's fire up our develop environment and take a look at GraphiQL to see what we have going on.</p> <div alt="GraphiQL query" data-embed-button="media" data-entity-embed-display="media_image" data-entity-type="media" data-entity-uuid="ea08daca-c426-4823-be5a-199d6ac056ba" data-langcode="en" title="GraphiQL" class="embedded-entity"> <img src="/sites/default/files/Screen%20Shot%202019-11-03%20at%206.54.59%20PM.png" alt="GraphiQL query" title="GraphiQL" typeof="foaf:Image" /></div> <p>As you can see, we now have access to <code>allBlockContentCtaBlock</code> in GraphiQL, but what are we going to do with it? Well, we are first going to create the CTA Block React component. We'll do that by creating the file <code>src/components/blocks/CtaBlock.js</code> and adding the following:</p> <pre> <code class="language-javascript">import React from 'react' import { graphql } from 'gatsby' const CtaBlock = ({ data }) =&gt; { return &lt;div class='ctaBlock'&gt; &lt;h3&gt;CTA Heading&lt;/h3&gt; &lt;p&gt;CTA Text goes here and here and here.&lt;/p&gt; &lt;a href="http://example.com"&gt;Link Text&lt;/a&gt; &lt;/div&gt; } export default CtaBlock </code></pre> <p>This is pretty simple and doesn't include anything having to do with our GraphQL query yet, but we have the structure in place. Now, let's look at the data we can pull from GraphQL. We want to get the heading, body, link, and content type, so our query is going to look something like this:</p> <pre> <code class="language-json">query MyQuery { allBlockContentCtaBlock { nodes { field_cta_heading field_cta_link { title uri } body { value } relationships { field_cta_content_type { name } } } } } </code></pre> <p>Which will give us back:</p> <pre> <code class="language-json">{ "data": { "allBlockContentCtaBlock": { "nodes": [ { "field_cta_heading": "Heading for the CTA Block", "field_cta_link": { "title": "Learn more!", "uri": "http://google.com" }, "body": { "value": "&lt;p&gt;Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Vestibulum facilisis, purus nec pulvinar iaculis, ligula mi congue nunc, vitae euismod ligula urna in dolor. Aenean massa.&lt;/p&gt;\r\n\r\n&lt;p&gt;Praesent nec nisl a purus blandit viverra. Nullam nulla eros, ultricies sit amet, nonummy id, imperdiet feugiat, pede. Phasellus dolor.&lt;/p&gt;\r\n" }, "relationships": { "field_cta_content_type": [ { "name": "Blog Post" } ] } } ] } } }</code></pre> <p>Perfect! Now I want you to notice a couple of things here. First, we're querying ALL of the CTA blocks here. Second, we're using the content type in the query. Let's get this into our component. Add in the query to our CtaBlock.</p> <pre> <code class="language-javascript">import React from 'react' import { graphql } from 'gatsby' const CtaBlock = ({ data }) =&gt; { return &lt;div class='ctaBlock'&gt; &lt;h3&gt;CTA Heading&lt;/h3&gt; &lt;p&gt;CTA Text goes here and here and here.&lt;/p&gt; &lt;a href="http://example.com"&gt;Link Text&lt;/a&gt; &lt;/div&gt; } export default CtaBlock export const CtaBlockQuery = graphql` allBlockContentCtaBlock { nodes { field_cta_heading field_cta_link { title uri } body { value } relationships { field_cta_content_type { name } } } }`</code></pre> <p>But wait, this isn't a page template, and it shouldn't be. What's the problem then? We have a query in a non-page template, and that's not good. What we need to do is figure out which of our templates are going to use this block and add it in there. Since we chose to have the block show on the blog post content type, we're going to use the blog post template. This will probably differ for your setup.</p> <p>Let's open up our page template and take a look:</p> <pre> <code class="language-javascript"> import React from 'react' import { graphql } from 'gatsby' import Layout from '../layouts' const BlogPostTemplate = ({ data }) =&gt; { const blogBody = data.nodeBlogPost.body.value return &lt;Layout&gt; &lt;h3&gt;{data.nodeBlogPost.title}&lt;/h3&gt; {data.nodeBlogPost.body.value} &lt;/Layout&gt; } export default BlogPostTemplate export const query = graphql` query($BlogPostID: String!){ nodeBlogPost(id: { eq: $BlogPostID }) { id title body { processed } } } ` </code></pre> <p>Now let's update this to have the block appear.</p> <pre> <code class="language-javascript">import React from 'react' import { graphql } from 'gatsby' import Layout from '../layouts' import CtaBlock from '../blocks/CtaBlock' const BlogPostTemplate = ({ data }) =&gt; { const blogBody = data.nodeBlogPost.body.value return &lt;Layout&gt; &lt;h3&gt;{data.nodeBlogPost.title}&lt;/h3&gt; {data.nodeBlogPost.body.value} &lt;CtaBlock /&gt; &lt;/Layout&gt; } export default BlogPostTemplate export const query = graphql` query($BlogPostID: String!){ nodeBlogPost(id: { eq: $BlogPostID }) { id title body { processed } } } ` </code></pre> <p>But we still need to include the query for the block itself. To do this, we're going to make a few edits to both of our components. We'll start with the CtaBlock component and convert the query into a fragment that we can reuse in multiple places if need be.</p> <pre> <code class="language-javascript">import React from 'react' import { graphql } from 'gatsby' const CtaBlock = ({ data }) =&gt; { return &lt;div class='ctaBlock'&gt; &lt;h3&gt;CTA Heading&lt;/h3&gt; &lt;p&gt;CTA Text goes here and here and here.&lt;/p&gt; &lt;a href="http://example.com"&gt;Link Text&lt;/a&gt; &lt;/div&gt; } export default CtaBlock export const CtaBlockQuery = graphql` fragment CtaBlockQuery on block_content__cta_block { field_cta_heading field_cta_link { title uri } body { value } relationships { field_cta_content_type { name } } }`</code></pre> <p>If you have a keen eye, you'll notice that we've removed the nodes section of the query. This is because we're now going to query for a single block. However, there is some risk to this. In the event that a content creator creates a new CTA block on Drupal for this content type instead of editing the existing one, the old one will remain in place because a single item query will only return a single item.</p> <p>Now, let's move back over to our page template and use this fragment to query for our block.</p> <pre> <code class="language-javascript">import React from 'react' import { graphql } from 'gatsby' import Layout from '../layouts' import CtaBlock from '../blocks/CtaBlock' const BlogPostTemplate = ({ data }) =&gt; { const blogBody = data.nodeBlogPost.body.value return &lt;Layout&gt; &lt;h3&gt;{data.nodeBlogPost.title}&lt;/h3&gt; {data.nodeBlogPost.body.value} &lt;CtaBlock /&gt; &lt;/Layout&gt; } export default BlogPostTemplate export const query = graphql` query($BlogPostID: String!){ nodeBlogPost(id: { eq: $BlogPostID }) { id title body { processed } } blockContentCtaBlock (relationships: {field_cta_content_types: {elemMatch: {name: {eq: "Blog Post"} } } } ){ ...CtaBlockQuery } } `</code></pre> <p>Take a look at what we've done here.  We've added in a query for the CtaBlock and we're filtering it by the content type it's attached to. After that, we're pulling in everything from the query on our component. This is exactly what we were wanting to do, but there's another step that we need to take to actually use the data on our component.</p> <p>If you look at the JSX element for</p> <pre> <code>&lt;CtaBlock /&gt;</code></pre> <p><br /> you'll notice we aren't passing anything to it, so we've got to make sure it has data to work with or we're going to end up rendering nothing.  Edit that line to be </p> <pre> <code>&lt;CtaBlock data={data} /&gt;</code></pre> <p>In case you're not familiar, this is a React concept known as passing props, or properties, to a child component. We're passing the data object that was returned from our GraphQL query to the CtaBlock component so that it can use the included data. Since this is just a demo, we're passing the entire thing along, but it's easy enough to only pass the relevant parts of the object.</p> <p>Now back in our CtaBlock component we can use the data to render out our block's content.</p> <pre> <code class="language-javascript">import React from 'react' import { graphql } from 'gatsby' const CtaBlock = ({ data }) =&gt; { return &lt;div class='ctaBlock'&gt; &lt;h3&gt;{data.blockContentCtaBlock.field_cta_heading}&lt;/h3&gt; &lt;p dangerouslySetInnerHtml= {{ __html: data.blockContentCtaBlock.body.value}} /&gt; &lt;a href={data.blockContentCtaBlock.field_cta_link.uri}&gt;{data.blockContentCtaBlock.field_cta_link.title}&lt;/a&gt; &lt;/div&gt; } export default CtaBlock export const CtaBlockQuery = graphql` fragment CtaBlockQuery on block_content__cta_block { field_cta_heading field_cta_link { title uri } body { value } relationships { field_cta_content_type { name } } }`</code></pre> <p>Now we have our block based on content type rendering within our content type's Gatsby template. Note that I've left out a few things that should be noted for decoupled sites. </p> <ol><li>An internal link should us the Gatsby &lt;Link /&gt; component.</li> <li>Drupal needs some love to pass the correct alias over for an internal link so that it renders correctly.</li> <li>This is a very basic example. YMMV.</li> </ol><p>Anyway, I hope that this helps someone out there who ran into the same problems that I did. Please feel free to reach out to me on Twitter @jddoesdev, on Slack where I'm usually Dorf, or just leave a comment here if you have questions, concerns, comments, or just something nice to say.</p> <p>Also, please feel free to support my efforts in speaking on mental health in tech or creating blog posts and tutorials like these by checking out my gofundme and patreon campaigns in the sidebar.</p> <p>Thanks for reading!</p> </div> <div class="field field--name-field-category field--type-entity-reference field--label-above"> <div class="field__label">Category</div> <div class="field__item"><a href="/development" hreflang="en">Development</a></div> </div> <div class="field field--name-field-tags field--type-entity-reference field--label-above"> <div class="field__label">Tags</div> <div class="field__items"> <div class="field__item"><a href="/taxonomy/term/12" hreflang="en">Drupal Planet</a></div> <div class="field__item"><a href="/taxonomy/term/11" hreflang="en">Drupal</a></div> <div class="field__item"><a href="/taxonomy/term/21" hreflang="en">GatsbyJS</a></div> <div class="field__item"><a href="/taxonomy/term/22" hreflang="en">decoupled</a></div> <div class="field__item"><a href="/taxonomy/term/23" hreflang="en">JSON:API</a></div> </div> </div> <div class="field field--name-field-comments field--type-disqus-comment field--label-visually_hidden"> <div class="field__label visually-hidden">Comments</div> <div class="field__item"><drupal-render-placeholder callback="Drupal\disqus\Element\Disqus::displayDisqusComments" arguments="0=Using%20Drupal%20Blocks%20in%20a%20Decoupled%20GatsbyJS%20Application&amp;1=https%3A//jamesdflynn.com/development/using-drupal-blocks-decoupled-gatsbyjs-application&amp;2=node/23" token="kiyq7yX2YgMGS3QzNbqPBmEgG79TlOysNhELvdFIVms"></drupal-render-placeholder></div> </div> <div class="field field--name-field-header-image-entity field--type-entity-reference field--label-hidden field__item"><article class="media media--type-image media--view-mode-embedded"> <div class="field field--name-image field--type-image field--label-hidden field__item"> <img src="/sites/default/files/negative-space-compiled-code-screen.jpg" width="1200" height="800" alt="Generic &quot;Look at my screen full of code&quot; screen" loading="lazy" typeof="foaf:Image" /> </div> </article> </div> Sun, 03 Nov 2019 18:40:45 +0000 jflynn 23 at https://jamesdflynn.com