Open-Source Pattern Library Tutorial: Part 3 — Building Pattern Pages

Marcello Paiva
cross.team
Published in
8 min readDec 20, 2019

In this tutorial we will configure Gatsby to automatically generate a page for each of our patterns and we’ll create a template that Gatsby will use to render those pages.

Modify Gatsby-Config

The first thing we need to do is install a new package. Our Gatsby project needs a plugin to allow GraphQL to recognize the pattern markdown files that are created and stored on our repo by Netlify-CMS. To install this package run the following command in your terminal in the root directory of your project:

yarn add gatsby-transformer-remark

Now let’s modify the gatsby-config.js file to look in the right place for our markdown files. In the plugins array you should see an object that has a resolve value of gatsby-source-filesystem. In the options for that object, find the two places where it says "images" and replace them with "pattern". Next, add gatsby-transformer-remark to the plugins array. When you're done the file should look like this:

module.exports = {
siteMetadata: {
title: `Pattern Library Tutorial`,
description: `A tutorial to show how to build a pattern library using Gatsby and Netlify-CMS`,
author: `@marcello.paiva`,
},
plugins: [
`gatsby-plugin-react-helmet`,
`gatsby-plugin-material-ui`,
`gatsby-plugin-netlify-cms`,
`gatsby-transformer-remark`,
{
resolve: `gatsby-source-filesystem`,
options: {
name: `pattern`,
path: `${__dirname}/src/pattern`,
},
},
`gatsby-transformer-sharp`,
`gatsby-plugin-sharp`,
{
resolve: `gatsby-plugin-manifest`,
options: {
name: `gatsby-starter-default`,
short_name: `starter`,
start_url: `/`,
background_color: `#663399`,
theme_color: `#663399`,
display: `minimal-ui`,
icon: `src/images/gatsby-icon.png`, // This path is relative to the root of the site.
},
},
// this (optional) plugin enables Progressive Web App + Offline functionality
// To learn more, visit: https://gatsby.dev/offline
// `gatsby-plugin-offline`,
],
}

Now find the src/components/image.js file and delete it. image.js was a component that came with the gatsby-starter code, and we won't be using it in our application. It expects the gatsby-config to look for files in the src/images directory, so if we try to leave it in the project after the changes we just made it will break the application.

Modify Gatsby-Node

Now we have to write some code in our gatsby-node.js file. This file is run during the app's build process allows us to do things like dynamically create pages, and add nodes to our GraphQL queries, and those are exactly the things we're going to configure it to do! Paste the following code into your gatsby-node.js file:

const path = require(`path`)
const { createFilePath } = require(`gatsby-source-filesystem`)

exports.onCreateNode = ({ node, getNode, actions }) => {
const { createNodeField } = actions
if (node.internal.type === `MarkdownRemark`) {
const slug = createFilePath({ node, getNode, basePath: `pattern` })
createNodeField({
node,
name: `slug`,
value: slug,
})
}
}

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

const patternTemplate = path.resolve(`src/templates/pattern.js`)

const result = await graphql(`
{
allMarkdownRemark(
sort: { order: DESC, fields: [frontmatter___date] }
limit: 1000
) {
edges {
node {
fields {
slug
}
}
}
}
}
`)

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

result.data.allMarkdownRemark.edges.forEach(({ node }) => {
createPage({
path: node.fields.slug,
component: patternTemplate,
context: {
// Data passed to context is available in page queries as GraphQL variables.
slug: node.fields.slug,
}, // additional data can be passed via context
})
})
}

What’s happening in this code is we are querying for our pattern markdown files and adding a node to each one. That node is a slug which is based on the title of our pattern. Then we create a page for each of our pattern markdown files, querying each one for the slug that we just added, and using that slug for the URL of that pattern’s page. It uses the src/templates/pattern.js file as the template for what’s rendered on the page, so let’s start building that now.

GraphQL is a query language that Gatsby utilizes to query data from a large variety of sources. It’s a powerful tool that helps bridge the gap between your front end and your data sources. I suggest taking some time to become familiar GraphQL here: https://graphql.org/learn/

Create Pattern Template

To build the pattern template we’re going to start by building two components that we are going to use in the final template, starting with the Banner.

Banner Component

Create a new file in your src/components/ directory called banner.js and paste the following code inside:

import React from 'react';
import PropTypes from 'prop-types';
import { withWidth, makeStyles, Paper, Typography, Chip } from '@material-ui/core';

const useStyles = makeStyles(theme => ({
title: {
margin: theme.spacing(2),
},
titleContainer: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
},
classifiers: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-around',
alignItems: 'flex-end',
marginRight: theme.spacing(1),
},
chipContainer: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
rightMargin: {
marginRight: theme.spacing(1),
},
categoryContainer: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
mobileContainer: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
banner: props => ({
width: '100%',
minHeight: '200px',
display: 'flex',
justifyContent: props.justify,
flexWrap: 'wrap',
backgroundColor: '#333333',
color: '#fff',
}),
}))

function Banner(props) {
const { width, frontmatter } = props
let styleProps = {}
if (width === 'xs' || width === 'sm') {
styleProps.justify = 'center'
} else {
styleProps.justify = 'space-between'
}
const classes = useStyles(styleProps)

return (
<Paper className={classes.banner}>
{ (width === 'xs' || width === 'sm') ? (
<div className={classes.mobileContainer}>
<Typography variant='h3' className={classes.title}>{ frontmatter.title ? frontmatter.title : '' }</Typography>
<Typography className={classes.title}>{ frontmatter.caption ? frontmatter.caption : '' }</Typography>
<div className={classes.categoryContainer}>
<Typography variant='caption' className={classes.rightMargin}>{ frontmatter.category ? frontmatter.category : '' }:</Typography>
<Typography variant='caption' className={classes.rightMargin}>{ frontmatter.subcategory ? frontmatter.subcategory : '' }</Typography>
</div>
<div className={classes.chipContainer}>
<Typography className={classes.rightMargin}>Tags:</Typography>
{ frontmatter.tags.map((tag) =>
<Chip label={tag} color="primary" className={classes.rightMargin} />
) }
</div>
</div>
) : (
<>
<div className={classes.titleContainer}>
<Typography variant='h3' className={classes.title}>{ frontmatter.title ? frontmatter.title : '' }</Typography>
<Typography className={classes.title}>{ frontmatter.caption ? frontmatter.caption : '' }</Typography>
</div>
<div className={classes.classifiers}>
<div className={classes.categoryContainer}>
<Typography variant='h6' className={classes.rightMargin}>Category:</Typography>
<Typography variant='h6' className={classes.rightMargin}>{ frontmatter.category ? frontmatter.category : '' }</Typography>
</div>
<div className={classes.categoryContainer}>
<Typography className={classes.rightMargin}>Subcategory:</Typography>
<Typography className={classes.rightMargin}>{ frontmatter.subcategory ? frontmatter.subcategory : '' }</Typography>
</div>
<div className={classes.chipContainer}>
<Typography className={classes.rightMargin}>Tags:</Typography>
{ frontmatter.tags.map((tag) =>
<Chip label={tag} color="primary" className={classes.rightMargin} />
) }
</div>
</div>
</>
)}
</Paper>
)
}

Banner.propTypes = {
width: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']).isRequired,
}

export default withWidth()(Banner)

The Banner component takes two props: width and frontmatter. The frontmatter prop contains all of the pattern data that the Banner will be rendering. We will pass it the frontmatter prop when we call the component in our pattern.js file. The width prop is a tool provided my Material-UI to build responsive applications using break points. The width prop gets provided to the Banner component in the export default withWidth()(Banner) line at the bottom of the file.

Content Component

Create a new file in your src/components/ directory called content.js and paste the following code inside:

import PropTypes from 'prop-types'
import React from 'react'
import { Paper, makeStyles, Typography } from '@material-ui/core'

const useStyles = makeStyles(theme => ({
container: {
backgroundColor: '#fff',
borderTop: 'solid 4px #ff8001',
marginTop: theme.spacing(4),
},
header: {
backgroundColor: '#333333',
color: '#fff',
padding: theme.spacing(2),
},
children: {
padding: theme.spacing(2),
}
}))

const Content = ({ title, children }) => {
const classes = useStyles()

return (
<Paper className={classes.container}>
<div className={classes.header}>
<Typography variant='h5'>{title}</Typography>
</div>
<div className={classes.children}>{children}</div>
</Paper>
)
}

Content.propTypes = {
title: PropTypes.string,
}

Content.defaultProps = {
title: ``,
}

export default Content

The Content component takes two props: title which is just a string that will be displayed as the title of that content block; and children which is just whatever content is nested within the Content component when it's called.

Completing Pattern Template

Now that we’ve finished building the components we’ll need to use in our pattern template, let’s create a new directory within src/ called templates/, and within that directory create pattern.js. But before we start writing in the code for our pattern template, we need to install a new dependency. We need to use the markdown npm package to convert the markdown data returned by some of our patterns' fields to HTML. To install it, run the following command in your terminal in the root directory of your project:

yarn add markdown

Now we can paste the following code into src/templates/pattern.js:

import React from 'react'
import { graphql } from 'gatsby'
import { markdown } from 'markdown'
import Content from '../components/content'
import Banner from '../components/banner'
import Layout from '../components/layout'
import { makeStyles, Typography, Grid } from '@material-ui/core'

const useStyles = makeStyles(theme => ({
img: {
width: '100%',
marginTop: theme.spacing(2),
}
}))

export default ({data}) => {
const classes = useStyles()
const { frontmatter } = data.markdownRemark // data.markdownRemark holds your post data

const assets = () => {
let result
if ( frontmatter.assets ) {
result = frontmatter.assets.map((asset) => (
<Content title='Assets'>
<img src={asset.asset.image} alt={asset.asset.caption} className={classes.img}/>
<Typography>{asset.asset.caption}</Typography>
</Content>
))
}
return result
}

const references = () => {
let result
if ( frontmatter.references ) {
result = frontmatter.references.map((reference) => (
<Content title='References'>
<h2>{reference.reference.title}</h2>
<Typography>{reference.reference.description}</Typography>
<a href={reference.reference.url}><Typography>{reference.reference.url}</Typography></a>
</Content>
))
}
return result
}

return (
<Layout >
<Banner frontmatter={frontmatter} />
<Grid container spacing={4}>
<Grid item xs={12} md={6}>
{frontmatter.problem ? (
<Content title='Problem'>
<div dangerouslySetInnerHTML={{ __html: markdown.toHTML(frontmatter.problem) }}></div>
</Content>
) : ''}
</Grid>
<Grid item xs={12} md={6}>
{frontmatter.solution ? (
<Content title='Solution'>
<div dangerouslySetInnerHTML={{ __html: markdown.toHTML(frontmatter.solution) }}></div>
</Content>
) : ''}
</Grid>
<Grid item xs={12} md={6}>
{frontmatter.usage ? (
<Content title='Usage'>
<div dangerouslySetInnerHTML={{ __html: markdown.toHTML(frontmatter.usage) }}></div>
</Content>
) : ''}
</Grid>
<Grid item xs={12} md={6}>
{frontmatter.accessibility ? (
<Content title='Accessibility'>
<div dangerouslySetInnerHTML={{ __html: markdown.toHTML(frontmatter.accessibility) }}></div>
</Content>
) : ''}
</Grid>
<Grid item xs={12} md={6}>
{assets()}
</Grid>
<Grid item xs={12} md={6}>
{references()}
</Grid>
</Grid>
</Layout>
)
}

export const query = graphql`
query($path: String!) {
markdownRemark(fields: { slug: { eq: $path } }) {
html
frontmatter {
title
caption
category
subcategory
tags
problem
solution
usage
accessibility
assets {
asset {
caption
image
}
}
references {
reference {
description
title
url
}
}
}
fields {
slug
}
}
}
`

As you can see at the bottom of this file, we have a GraphQL query that is grabbing all of the necessary data from a specific pattern based on its slug. The data that is retrieved by the query is provided to the component by the data prop. We then use that data prop to populate all of the content on the page, using the markdown.toHTML() function to convert the markdown data from the problem, solution, usage, and accessibility fields into HTML that can be rendered by our React app.

View Pattern Page

Now that you’ve finished building the pattern page for your application, let’s go ahead and view it. Push all of the code you just wrote to master and let Netlify deploy your changes. Once it’s done, go to your Netlify site and in the URL add /file-name to the URL where "file-name" is the name of a markdown file that's stored in the src/pattern/ directory of your repo.

In my case, the file was named my-first-pattern.md and the URL for its page was https://plib-tutorial.netlify.com/my-first-pattern/. If everything worked like it should, you should see a page that looks like this:

Pattern page

Right now, however, there is no way for a user to be able to access any of the pattern pages from the home page. In the next part of the tutorial, we’re going to work on building a searchable list that users can select from to bring them to their desired pattern.

Part 4 — Searchable List

If you are having issues or want to look at the source code for this part, you can checkout the part3 branch of the plib-tutorial repo:

--

--

Marcello Paiva
cross.team

I am a front-end web developer with a passion for accessibility!