skip to Main Content

I’m using Strapi as a Headless CMS and building my frontend with Gatsby + Graphql. I have a "blocks renderer" component that is rendering any of the dynamic zones in strapi.

import React from "react"
import { graphql } from "gatsby"
import BlockHero from "./block-hero"
import BlockParagraph from "./block-paragraph"
import BlockSplitFeature from "./block-split-feature"

const componentsMap = {
    // STRAPI__COMPONENT_LAYOUT_ELEMENTS_MULTIPLE_CALLOUT: blockMultipleCallout,
    STRAPI__COMPONENT_LAYOUT_ELEMENTS_SIMPLE_PARAGRAPH: BlockParagraph,
    STRAPI__COMPONENT_LAYOUT_ELEMENTS_SPLIT_FEATURE: BlockSplitFeature,
    STRAPI__COMPONENT_MEDIA_ELEMENT_HERO: BlockHero,
    // STRAPI__COMPONENT_META_DATA_DEFAULT_SEO: blockSeo
}

const Block = ({ block }) => {
    const Component = componentsMap[block.__typename]

    if(!Component) {
        return null
    }

    return <Component data={block} />
}

const BlocksRenderer = ({ blocks }) => {
    return (
        <div>
            {blocks.map((block, index) => (
                <Block key={`${index}${block.__typename}`} block={block} />
            ))}
        </div>
    )
}

export const query = graphql`
    fragment Blocks on STRAPI__COMPONENT_LAYOUT_ELEMENTS_CTASTRAPI__COMPONENT_LAYOUT_ELEMENTS_MULTIPLE_CALLOUTSTRAPI__COMPONENT_LAYOUT_ELEMENTS_SIMPLE_PARAGRAPHSTRAPI__COMPONENT_LAYOUT_ELEMENTS_SPLIT_FEATURESTRAPI__COMPONENT_MEDIA_ELEMENT_HEROUnion {
        __typename
        ... on STRAPI__COMPONENT_LAYOUT_ELEMENTS_MULTIPLE_CALLOUT {
            id
            MultipleCalloutItem {
                id
                Heading
                Description
            }
          }
          ... on STRAPI__COMPONENT_LAYOUT_ELEMENTS_SIMPLE_PARAGRAPH {
            id
            Text
          }
          ... on STRAPI__COMPONENT_LAYOUT_ELEMENTS_SPLIT_FEATURE {
            id
            Heading
            Description
            mediaAlignment
            Media {
                id
                mime
                localFile {
                    childImageSharp {
                        gatsbyImageData
                      }
                }
                alternativeText
            }
          }
          ... on STRAPI__COMPONENT_MEDIA_ELEMENT_HERO {
            id
            Heading
            Description
            Media {
                id
                mime
                alternativeText
                localFile {
                    url
                }
                alternativeText
            }
          }
    }
`

export default BlocksRenderer

Then I have my page layout file to generate a page layout (side note, the "Layout" element is just for the navigation & footer. This will be rewritten once I have this page layout file issue fixed)>

import React from "react"
import { useStaticQuery, graphql } from "gatsby"
import Layout from "../components/layout"
import Seo from "../components/seo"
import BlocksRenderer from "../components/blocks-renderer"

const PageLayout = () => {
    const { allStrapiPage } = useStaticQuery(graphql`
        query {
            allStrapiPage {
                edges {
                    node {
                        id
                        Name
                        Slug
                        Blocks {
                            ...Blocks
                        }
                    }
                }
            }
        }
    `)
    const { Blocks } = allStrapiPage

    return (
        <Layout>
            <div>{allStrapiPage.id}</div>
            <h1>{allStrapiPage.Name}</h1>
            <BlocksRenderer blocks={allStrapiPage.Blocks} />
        </Layout>
    )
}

export default PageLayout

I’m dynamically creating pages with a gatsby-node.js file. When I try to access one of the dynamically created slugs, I get an error in the blocks-renderer file that says can’t access property "map", blocks is undefined. Anyone have any ideas?

EDIT: Added the additional files mentioned.

gatsby-config.js file below:

/**
 * Configure your Gatsby site with this file.
 *
 * See: https://www.gatsbyjs.com/docs/gatsby-config/
 */

require("dotenv").config({
  path: `.env.${process.env.NODE_ENV}`,
})

module.exports = {
  /* Your site config here */
  plugins: [
    "gatsby-plugin-gatsby-cloud",
    "gatsby-plugin-postcss",
    "gatsby-plugin-sass",
    "gatsby-plugin-image",
    "gatsby-plugin-sharp",
    "gatsby-transformer-sharp",
    "gatsby-transformer-remark",
    {
      resolve: "gatsby-source-strapi",
      options: {
        apiURL: process.env.STRAPI_API_URL || "http://localhost:1337",
        accessToken: process.env.STRAPI_TOKEN,
        collectionTypes: [ 
        "drink", 
        "category", 
        {
          singularName: "page",
          queryParams: {
            populate: {
              Blocks: {
                populate: "*",
                MultipleCalloutItem: {
                  populate: "*",
                },
              },
              PageMeta: {
                populate: "*",
              },
              ParentPage: {
                populate: "*",
              },
            },
          },
        },
      ],
        singleTypes: [
          {
            singularName: "global",
            queryParams: {
              populate: {
                DefaultSeo: {
                  populate: "*",
                },
                Favicon: {
                  populate: "*",
                },
              },
            },
          },
          {
            singularName: "homepage",
            queryParams: {
              populate: {
                Blocks: {
                  populate: "*",
                },
              },
            },
          },
        ],
        queryLimit: 1000,
      }
    },
  ],
}

home.js (which works as intended).

import React from "react"
import { useStaticQuery, graphql } from "gatsby"
import Layout from "../components/layout"
import Seo from "../components/seo"
import BlocksRenderer from "../components/blocks-renderer"

const HomePage = () => {
    const { strapiHomepage } = useStaticQuery(graphql`
        query {
            strapiHomepage {
                Blocks {
                    ...Blocks
                }
            }
        }
    `)
    const { Blocks } = strapiHomepage

    // const seo = {
    //     metaTitle: title,
    //     metaDescription: title
    // }

    return (
        <Layout>
            <BlocksRenderer blocks={Blocks} />
        </Layout>
    )
}

export default HomePage

This is the gatsby-node.js file I’m using to generate the pages with the page-layout.js file. Note that I can generate the pages and content, minus the Blocks query.

const path = require('path')

exports.createPages = async ({ graphql, actions, reporter }) => {
    const { createPage } = actions

    const result = await graphql(
        `
        query {
            allStrapiPage {
                edges {
                    node {
                        Slug
                        Name
                        ParentPage {
                            Slug
                        }
                    }
                }
            }
        }
    `
    )

    if (result.errors) {
        reporter.panicOnBuild(`Error while running GraphQL Query`)
        return
    }

    const pageTemplate = path.resolve(`./src/layouts/page-layout.js`)
    result.data.allStrapiPage.edges.forEach(({ node }) => {
        const path = node.Slug
        createPage({
            path,
            component: pageTemplate,
            context: {
                pagePath: path
            },
        })
    })
}

2

Answers


  1. Chosen as BEST ANSWER

    Thanks to @Ferran I was pointed in the right direction and solved this issue.

    Two changes needed to be made for this to work properly. First, I needed to be passing pageContext from the gatsby-node.js file. Here I'm passing the page slug to the template

    const { resolve } = require('path')
    const path = require('path')
    
    exports.createPages = async ({ graphql, actions, reporter }) => {
        const { createPage } = actions
    
        const result = await graphql(
            `
            query {
                allStrapiPage {
                    edges {
                        node {
                            Slug
                            Name
                            ParentPage {
                                Slug
                            }
                        }
                    }
                }
            }
        `
        )
    
        if (result.errors) {
            reporter.panicOnBuild(`Error while running GraphQL Query`)
            return
        }
    
        const pageTemplate = path.resolve('./src/layouts/page-layout.js')
        result.data.allStrapiPage.edges.forEach(edge => {
            const path = `${edge.node.Slug}`
            const parentPath = `${edge.node.ParentPage.Slug}`
            createPage({
                path,
                component: pageTemplate,
                context: {
                    Slug: edge.node.Slug
                },
            })
            resolve()
        })
    }
    

    Then in the page-layout.js file, I needed to get the pageContext from gatsby-node.js, map all of my page nodes, in the graphql query, and pass the page Slug from gatsby-node.js as a variable in the graphql query.

    import React from "react"
    import { useStaticQuery, graphql } from "gatsby"
    import Layout from "../components/layout"
    import Seo from "../components/seo"
    import BlocksRenderer from "../components/blocks-renderer"
    
    const PageLayout = ({ data, pageContext }) => {
        const { Slug } = pageContext
    
        console.log(Slug)
    
        return (
            <Layout>
                {
                    data.allStrapiPage.nodes.map(node => {
                        return (
                            <div key={node.id}>
                            <h1>{node.Name}</h1>
                            {node.Blocks &&
                                <BlocksRenderer blocks={node.Blocks} />
                            }
                        </div>
                        )
                        
                    })
                }
            </Layout>
        )
    }
    
    export const query = graphql`
            query GetPage($Slug: String) {
                allStrapiPage(filter: { Slug: {in: [$Slug]} }) {
                    nodes {
                        id
                        Name
                        Blocks {
                            ...Blocks
                        }
                    }
                }
            }
        `
    
    export default PageLayout
    

    Now I can dynamically create pages with Strapi and the "blocks" I made using dynamic zones.


  2. The problem is here:

    <BlocksRenderer blocks={allStrapiPage.Blocks} />
    

    You can’t access directly to Blocks because you node is an array inside edges property. From what I see, the loop is done in BlocksRenderer hence you need to provide it an array of blocks. Without knowing exactly your data structure and what returns it’s difficult to guess but try something like:

    <BlocksRenderer blocks={allStrapiPage.edges.node[0].Blocks} />
    

    I have a Home.js file that is using a different property
    (allStrapiHomepage) and BlockRenderer is working as expected

    If your Home.js query is using a page query instead of a static query they cane be triggered and hydrated in different runtimes and build times, so one can fail if the other doesn’t. This leads me to think that maybe the query is ok, but the logic isn’t. You can easily check it by adding a simple condition like:

    <BlocksRenderer blocks={allStrapiPage?.Blocks} />
    

    Or:

    {allStrapiPage.Blocks && <BlocksRenderer blocks={allStrapiPage.Blocks} />}
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search