Search code examples
gatsbyreact-helmet

Dynamically adding page meta in Gatsby Layout component using markdown


My objective: Add code to the Layout component so that the Meta title, description, and image are updated with the current page the user is on based on content provided in the frontmatter. For example if the user navigates to the about page, the meta should display the following:

<title>About | Website</title>
<meta content="This is the about page" name="description">
<link rel="canonical" href="www.website.com/about" />
<meta property="og:image" content="/images/about-meta.png" />

While on the about page the corresponding meta is as such:

---
title: About
description: This is the about page
meta_image: /images/about-meta.png
slug: about
---

What I'm doing now: I'm adding all that information in a Helmet tag to the individual templates and I have the title template setup in the layout, but it seems like there's got to be a better way to just automatically set the Helmet component in the layout to read that.

Layout.jsx

<Helmet
  defaultTitle={siteMetadata.title}
  titleTemplate={`%s | ${siteMetadata.title}`}
/>

templates/about.jsx

<Helmet title={frontmatter.title} >
  <meta property="og:image" content={frontmatter.meta_image}/>
  <link rel="canonical" href="{siteMetadata.siteUrl}/{frontmatter.slug}" />
</Helmet>

Gatsby has this guide on creating a SEO component, but it doesn't to include how that works from the page template and uses jargon to define jargon so it's not particularly useful to me as a react noob. I'm not quite understanding what PropTypes are or what they do.

What I'm trying: I'd like to add a static query within the layout that will detect the current page slug then fetch the markdownremark based on the current slug and dynamically fill in the meta data so I don't have to add a helmet tag per page template.


Solution

  • What I'm trying: I'd like to add a static query within the layout that will detect the current page slug then fetch the markdownremark based on the current slug and dynamically fill in the meta data so I don't have to add a helmet tag per page template.

    This approach is valid but when you try to avoid adding an SEO component in each page template, you are changing the responsibilities of your components. In my opinion, is in the SEO component where you must place the useStaticQuery (or StaticQuery) what will be triggered in each page change, not in the Layout which its only responsibility is to display shared layout across the site, not querying. Moreover, you will probably have a page query in the isolated pages (about, etc) hence you are already fetching data so there's no point to don't adding a few extra lines to add the SEO there.

    Furthermore, you won't be able to add a query in your Layout because you only have two options:

    • Add a page query: this is not an option because it only works in top-level components (pages or templates). Layout is none.
    • Add a useStaticQuery in the Layout: also not an option because you can't add dynamic parameters (like the slug or similar). It's a known limitation.

    In fact, it's on the page where you know exactly what are the metadata of the page itself, so it's easier and cleanest to use the SEO component on each specific page you want to have all the SEO metadata.

    If you query things in the Layout, you will be lifting props unnecessarily because they will be always fetched, changed, and drilled down.

    Creating a SEO component like:

    const SEO = ({ description, lang= "en-US", meta, title }) => {
      const { site } = useStaticQuery(graphql`
                  query getAllMetadata {
                      site {
                          siteMetadata {
                              title
                              description
                              author
                          }
                      }
                  }
        `,
      );
      const metaDescription = description ?? site.siteMetadata.description;
    
      return <Helmet htmlAttributes={{ lang }} title={title} titleTemplate={`%s | ${site.siteMetadata.title}`}>
        <meta name={`robots`} content={`index, follow`}/>
        <meta name={`description`} content={metaDescription}/>
        <meta name={`og:title`} content={title}/>
        <meta name={`og:description`} content={metaDescription}/>
        <meta name={`og:type`} content={`website`}/>
        <meta name={`twitter:card`} content={`summary`}/>
        <meta name={`twitter:creator`} content={site.siteMetadata.author}/>
        <meta name={`twitter:title`} content={title}/>
        <meta name={`twitter:description`} content={metaDescription}/>
      </Helmet>;
    };
    
    export default SEO;
    

    You will be querying the shared metadata that specifically in the component (title, description and author in this case, added in the siteMetadata object in the gatsby-config.js) while at the same time, the component it's still receiving the needed data (as props) for each specific field for each specific page where it's placed.