Gatsby GraphQL Fragments
This is a 4 part series about building SEO-optimized Gatsby blog.
In Part 2, we will create five page components and learn about GraphQL fragments and aliases.
We will also create the Layout
, Nav
components, and MDX blog posts with frontmatter.
At any point of time feel free to checkout the source code in GitHub or the live blog.
Let's take a few moments to familiarize ourselves with the project structure.
Click on the folder to expand/collapse- articles
- golden-retriver
- cover.jpg
- index.mdx
- pug
- cover.jpg
- index.mdx
- siberian-husky
- cover.jpg
- index.mdx
- src
- images
- about.jpg
- blog.jpg
- contact.jpg
- home.jpg
- components
- Layout
- index.jsx
- Nav
- index.jsx
- SEO
- DefaultMeta.jsx
- OpenGraph.jsx
- SchemaOrg.jsx
- Twitter.jsx
- getActivePages.js
- getCurrentUrl.js
- getImageUrls.js
- index.jsx
- fragments
- FrontmatterFields.js
- ImageUrlFields.js
- helpers
- slashify.js
- hooks
- useSiteMetadata.js
- pages
- about.jsx
- blog.jsx
- contact.jsx
- index.jsx
- templates
- article.jsx
- static
- logo.jpg
- .nvmrc
- .env.development
- .env.production
- site-metadata.js
- gatsby-config.js
- gatsby-node.js
- package.json
Page Components and URL Structure Overview
The blog we are building consists of five pages:
Page | URL path | Component path |
---|---|---|
Home | / | src/pages/index.jsx |
Blog | /blog/ | src/pages/blog.jsx |
Article | /article/ | src/templates/article.jsx |
Contact | /contact/ | src/pages/contact.jsx |
About | /about/ | src/pages/about.jsx |
Each one of these pages will include Open Graph, Twitter, and Schema.org markup
that will be rendered by the SEO
component. Every blog post has its folder with
index.mdx
and a cover image.
The Home Page Component
All page components are stored in the src/pages
directory.
/** @jsx jsx */import { jsx, Themed } from "theme-ui"import { graphql } from "gatsby"import useSiteMetadata from "../hooks/useSiteMetadata"import Layout from "../components/Layout"const Home = ({data: {file: { childImageSharp: seoImages },},}) => {const {pages: {home: { id, title, description, imageAlt, breadcrumb, type },},} = useSiteMetadata()return (<LayoutpageId={id}title={title}description={description}images={{ ...seoImages }}imageAlt={imageAlt}breadcrumb={breadcrumb}type={type}><Themed.h1>Hi! My name is Jane.</Themed.h1>// ✂️</Layout>)}export const query = graphql`query ($image: String) {file(absolutePath: { eq: $image }) {childImageSharp {...ImageUrlFields}}}`export default Home
We import graphql
from gatsby
and then we export
a new constant called query
(though its name can be anything). graphql
is a tagged template function and isn't executed
the same way as typical JavaScript code. During the build step, Gatsby converts the source code to an
abstract syntax tree (AST) and removes graphql
-tagged
templates from the original source code. For these reasons, we can't use expression interpolation. However, we
can still pass variables to the queries via the context
argument of the createPages
action. This is exactly what we
did earlier with image
absolute file path in gatsby-node.js
.
And that is why it's available as $image
inside of the query. Keep in mind that you can only
have one GraphQL query per file.
ImageUrlFields
is the GraphQL fragment. GraphQL fragments are logical chunks of code that can be shared between
multiple queries. Every fragment includes a subset of fields that belong to its associated type. GraphQL fragment
syntax looks like JavaScript's spread operator and has a similar purpose: to assign the keys and values of one object
to another object.
The result of the query is automatically inserted into the Home
component as the data
prop. The rest of the page
data is retrieved from site-metadata.js via
the useSiteMetadata
hook.
The ImageUrlFields GraphQL Fragment
Facebook, Twitter, Google all have different requirements for the size and aspect ratio of their cover images:
- Facebook recommends images to be at least 1200x630 pixels.
- Twitter recommends an aspect ratio of 2:1 with minimum dimensions of 300x157 pixels.
- Google recommends images to be at least 696 pixels wide and suggests providing images with 16x9, 4x3, and 1x1 aspect ratios.
We are going to use 1600 pixels as our baseline width.
Let's create ImageUrlFields.js
file in src/fragments
:
import { graphql } from "gatsby"export const ImageUrlFields = graphql`fragment ImageUrlFields on ImageSharp {twitter: gatsbyImageData(layout: FIXEDwidth: 1600height: 800formats: [JPG])openGraph: gatsbyImageData(layout: FIXEDwidth: 1600height: 838formats: [JPG])schemaOrg1x1: gatsbyImageData(layout: FIXEDwidth: 1600height: 1600formats: [JPG])schemaOrg4x3: gatsbyImageData(layout: FIXEDwidth: 1600height: 1200formats: [JPG])schemaOrg16x9: gatsbyImageData(layout: FIXEDwidth: 1600height: 900formats: [JPG])}`
twitter
, openGraph
, schemaOrg1x1
, schemaOrg4x3
and schemaOrg16x9
are GraphQL aliases. Aliases are used
to rename the returned data fields. Since the result object fields will match the name of the field (gatsbyImageData
)
we can't query for the same field with different arguments. The solution is to alias them to different names. This
way, we can get all results in one request.
To create a fragment, define it in a query and export it as a named export
, which makes it globally available
(there is no need to import fragments before using them) regardless of its location in the project. All
fragments must have unique names.
The Contact Page Component
The Contact
page is almost identical to the Home
page component. The
only difference is that we fetch siteMetadata.pages.contact
instead of siteMetadata.pages.home
.
/** @jsx jsx */import { jsx, Themed } from "theme-ui"import { graphql } from "gatsby"import useSiteMetadata from "../hooks/useSiteMetadata"import Layout from "../components/Layout"const Contact = ({data: {file: { childImageSharp: seoImages },},}) => {const {pages: {contact: { id, pathName, title, description, imageAlt, breadcrumb, type },},} = useSiteMetadata()return (<LayoutpageId={id}pathName={pathName}title={title}description={description}images={{ ...seoImages }}imageAlt={imageAlt}breadcrumb={breadcrumb}type={type}><Themed.h1>{title}</Themed.h1>// ✂️</Layout>)}export const query = graphql`query ($image: String) {file(absolutePath: { eq: $image }) {childImageSharp {...ImageUrlFields}}}`export default Contact
The About Page Component
The About
page is almost identical to the Home
page component,
but this time we fetch siteMetadata.pages.about
instead of siteMetadata.pages.home
.
/** @jsx jsx */import { jsx, Themed } from "theme-ui"import { graphql } from "gatsby"import useSiteMetadata from "../hooks/useSiteMetadata"import Layout from "../components/Layout"const About = ({data: {file: { childImageSharp: seoImages },},}) => {const {pages: {about: { id, pathName, title, description, imageAlt, breadcrumb, type },},} = useSiteMetadata()return (<LayoutpageId={id}pathName={pathName}title={title}description={description}images={{ ...seoImages }}imageAlt={imageAlt}breadcrumb={breadcrumb}type={type}><Themed.h1>{title}</Themed.h1>// ✂️</Layout>)}export const query = graphql`query ($image: String) {file(absolutePath: { eq: $image }) {childImageSharp {...ImageUrlFields}}}`export default About
The Blog Page Component
In the Blog
page component, we first need to get all the MDX nodes (our articles) and
sort them in descending order by the
frontmatter___published
field (as defined in the article's frontmatter). After this, we query for
the slug
node and FrontmatterFields
(GraphQL fragment) fields
of the MDX nodes and loop over them to generate a list of all the articles.
/** @jsx jsx */import { jsx, Themed } from "theme-ui"import { graphql } from "gatsby"import { GatsbyImage } from "gatsby-plugin-image"import useSiteMetadata from "../hooks/useSiteMetadata"import Layout from "../components/Layout"import Link from "../components/Link"import slashify from "../helpers/slashify"const Blog = ({data: {allMdx,file: { childImageSharp: seoImages },},}) => {const {pages: {blog: { id, pathName, title, description, imageAlt, breadcrumb, type },},} = useSiteMetadata()return (<LayoutpageId={id}pathName={pathName}title={title}description={description}images={{ ...seoImages }}imageAlt={imageAlt}breadcrumb={breadcrumb}type={type}><Themed.h1>{title}</Themed.h1><ul>{allMdx.edges.map(({node: {id: key,fields: { slug },frontmatter: {title,description,published,image: {childImageSharp: { gatsbyImageData: coverImage },},imageAlt,},},}) => (<li key={key}><Link to={slashify(pathName, slug)}><Themed.h2>{title}</Themed.h2></Link><GatsbyImage image={coverImage} alt={imageAlt} /><time>{published}</time><Themed.p>{description}</Themed.p></li>))}</ul></Layout>)}export const query = graphql`query ($image: String) {file(absolutePath: { eq: $image }) {childImageSharp {...ImageUrlFields}}allMdx(sort: { order: DESC, fields: [frontmatter___published] }) {edges {node {idfields {slug}frontmatter {...FrontmatterFields}}}}}`export default Blog
The FrontmatterFields GraphQL Fragment
FrontmatterFields
is a reusable piece of code that contains all the frontmatter fields of an MDX article. Here is
an example of the article's frontmatter:
---title: Golden Retrieverdescription: Awesome article about Golden Retrieverpublished: 2019-01-01modified: 2019-03-01image: ./cover.jpgimageAlt: Golden Retriever puppy---
Let's create the FrontmatterFields.js
file in src/fragments
:
import { graphql } from "gatsby"export const FrontmatterFields = graphql`fragment FrontmatterFields on MdxFrontmatter {titledescriptionpublished(formatString: "MMMM DD, YYYY")modified(formatString: "MMMM DD, YYYY")image {childImageSharp {gatsbyImageData(width: 800placeholder: BLURREDformats: [AUTO, WEBP, AVIF])}}imageAlt}`
Since the published
and modified
fields use the Date
type, we can apply the formatString
function to them and
use Moment.js syntax to format the date strings.
image
is a file path that was converted to a GraphQL File
node, and because it's an image, we can access
the childImageSharp
field. To configure image sizes, loading effect, and file formats, we pass arguments inside
of the gatsbyImageData
resolver. By setting the width
, we are limiting the maximum image size. Gatsby image
components are lazy-loaded by default. To ensure that the layout does not jump around, a placeholder is displayed
before the image loads. The BLURRED
option generates a low-resolution version of the source image and displays it as
a blurred background. The Gatsby Image plugin
supports four output formats: JPEG, PNG, WebP, and AVIF. AUTO
means the plugin will generate images in the
same format as the source image. In addition to this we also specify WebP
and the new AVIF
format (currently, AVIF
has
limited browser support).
The Article Page Component
The Article
component receives the MDX file and renders it.
/** @jsx jsx */import { jsx, Themed, Flex } from "theme-ui"import { graphql } from "gatsby"import { MDXRenderer } from "gatsby-plugin-mdx"import { GatsbyImage } from "gatsby-plugin-image"import useSiteMetadata from "../hooks/useSiteMetadata"import Layout from "../components/Layout"const Article = ({data: {mdx: {body,frontmatter: {title,description,image: {childImageSharp: { gatsbyImageData: coverImage, ...seoImages },},imageAlt,published,modified,},},},pageContext: { slug },}) => {const {pages: {article: { id, type },},} = useSiteMetadata()return (<LayoutpageId={id}type={type}slug={slug}title={title}description={description}images={{ ...seoImages }}imageAlt={imageAlt}published={published}modified={modified}><Flex><GatsbyImage image={coverImage} alt={imageAlt} /><Themed.p>Published on {published}</Themed.p>{modified && <Themed.p>Updated on {modified}</Themed.p>}<MDXRenderer>{body}</MDXRenderer></Flex></Layout>)}export const query = graphql`query ($id: String!) {mdx(id: { eq: $id }) {bodyfrontmatter {...FrontmatterFieldsimage {childImageSharp {...ImageUrlFields}}}}}`export default Article
We query MDX data by id
inside of the graphql
string (id
is accessible inside of the query because we passed it in
context
). Then we spread the FrontmatterFields
fragment in
the frontmatter
node to get every frontmatter field. To get SEO images we spread the ImageUrlFields
fragment in the
childImageSharp
node. The rest of the data is queried with the useSiteMetadata
hook.
In the Article
component, we pass the body
(all MDX file content without frontmatter data) to the MDXRenderer,
which is a
component that takes compiled MDX content and renders it.
The Layout Component
The Layout
component in Gatsby is used to share common styles and components between pages. We use it to include
Nav
and SEO
components for every page.
/** @jsx jsx */import { jsx, Flex } from "theme-ui"import { Fragment } from "react"import SEO from "../SEO"import Nav from "../Nav"const Layout = ({children,pageId,pathName,slug,title,description,images,imageAlt,breadcrumb,published,modified,type,}) => {return (<Fragment>{pageId && (<SEOpageId={pageId}pathName={pathName}slug={slug}title={title}description={description}images={images}imageAlt={imageAlt}breadcrumb={breadcrumb}published={published}modified={modified}type={type}/>)}<Flex><Nav /><main data-speakable="true">{children}</main></Flex></Fragment>)}export default Layout
Not every page will require an SEO
component. If you ever decide to add a 404 page, make sure
that the SEO
component is not rendered there. That's why we are using the pageId
prop from
site-metadata.js
which is defined only for
those pages that contain an SEO
component.
We append data-speakable="true"
to the main
tag in order to let assistive technologies know
that the content inside of the page is suitable for Text-to-Speech interpretation.
The Nav Component
In the Nav
component, we use useSiteMetadata
hook to
get all of the pathnames. Then we format them with slashify and
pass them as a to
prop of a Gatsby Link
component.
/** @jsx jsx */import { jsx } from "theme-ui"import { Link } from "gatsby"import useSiteMetadata from "../../hooks/useSiteMetadata"import slashify from "../../helpers/slashify"const Nav = () => {const {pages: {home: { pathName: homePathName },blog: { pathName: blogPathName },contact: { pathName: contactPathName },about: { pathName: aboutPathName },},} = useSiteMetadata()return (<ul><li><Link to={slashify(homePathName)}>Home</Link></li><li><Link to={slashify(blogPathName)}>Blog</Link></li><li><Link to={slashify(contactPathName)}>Contact</Link></li><li><Link to={slashify(aboutPathName)}>About</Link></li></ul>)}export default Nav
The MDX Articles
The very last step is to create a few MDX files that will be used to render the articles. Don't forget to add frontmatter to the top of each file and cover images. As a reference, you can see the Pug article file.