Building Up the Gatsby Project with Markdown Content
Gatsby framework offers multiple ways to create routes/pages. In this chapter, we'll use "Gatsby Node APIs" to create pages from markdown content that we'll have generated by Netlify CMS. We'll also add a "page template selecting logic" to auto-select custom templates for the pages created. Lastly, we'll split the UI by adding React components for the common parts.
This post is the 2nd chapter of a 7-chapter article series “Going Local and Free with Gatsby & Netlify CMS”.
In the previous chapter, we created a minimal working project with Gatsby & Netlify CMS. At this point, you should at least have a working project to continue to Chapter 2.
Part 4 - Sourcing and Transforming Markdown Content
At this point, we have a DB-like environment that holds our data in markdown files and have a running Gatsby environment. Now, we need to read this data from the files and parse the markdown content. To do so, we need to do two things.
- Source those CMS-generated files from the file system with a
gatsby-source-*
plugin. - Transform those content into JavaScript objects by parsing them with various
gatsby-transformer-*
plugins.
Since we used the filesystem as the “source” and markdown files as file “format”, we need gatsby-source-filesystem
and gatsby-transformer-remark
plugins for these operations. Now install these plugins with the npm install
command.
# Terminal window at project root
npm i gatsby-source-filesystem gatsby-transformer-remark
Then add plugins into the Gatsby configuration file’s plugins array.
// gatsby-config.js
module.exports = {
plugins: [
{
resolve: `gatsby-source-filesystem`,
options: {
path: `${__dirname}/src/content/`,
name: `content`,
},
},
`gatsby-transformer-remark`,
// ...
]
}
gatsby-source-filesystem
plugin allows having multiple instances of itself in theplugins
array. Above, we sourced thesrc/content
directory with the name “content”. We’ll have another instance for the “pages” later.
After adding plugins, reload the Gatsby development server and revisit the http://localhost:8000/___graphql
address (Or reload.). You’ll see two additional options on the GraphiQL explorer. allMarkdownRemark
and markdownRemark
. Both introduced by the gatsby-transformer-remark
plugin.
Now, run an empty allMarkdownRemark
query to see the sourced and transformed content collection. (Simply expand allMarkdownRemark
on the list and hit the “Execute Query
” button with a “Play” icon on the navbar. Or use ctrl|cmd + enter
.
# GraphiQL - query
query MyQuery {
allMarkdownRemark {
edges {
node {
id
}
}
}
}
The result would be a JavaScript
object like this.
// GraphiQL - query result
{
"data": {
"allMarkdownRemark": {
"edges": [
{
"node": {
"id": "..."
}
}
]
}
},
"extensions": {}
}
That means we’ve successfully sourced and transformed our markdown content into the Gatsby. To be clear, include frontmatter
data into the query and re-run it.
# GraphiQL - query
query MyQuery {
allMarkdownRemark {
edges {
node {
id
frontmatter {
title
contentKey
}
}
}
}
}
Now see our single content item src/content/designs/hello-from-design-01.md
read from the file system and parsed as a JavaScript
object.
// GraphiQL - query result
{
"data": {
"allMarkdownRemark": {
"edges": [
{
"node": {
"id": "fef329b0-1b07-570c-860b-07d195737f17",
"frontmatter": {
"title": "hello from design 01",
"contentKey": "design"
}
}
}
]
}
},
"extensions": {}
}
Without a transformer plugin, we may still read sourced files with allFile
and file
queries. (If we’ve sourced them into Gatsby.) But the result will not include the file content. Most importantly, the “parsed” file content of the sourced files will be missing. This is why we need a transformer plugin to transform data types into each other.
Source plugins allow us to read and query content from remote or local locations. (From the file system in our case.)
Transformer plugins allow us to transform read data into
JavaScript
objects returned from GraphQL queries.
You might notice that “GraphQL” has two types of query options for collections,
edges
andnodes
. They are almost the same. However,edges
has additional data about the node location it contains, which are the references to thenext
andprevious
nodes relative to the currentNode
.
Update Summary
Files Updated | Notable Changes | |
---|---|---|
+/- | gatsby-config.js |
Added gatsby-source-filesystem and gatsby-transformer-remark plugins to the plugins array and sourced src/content directory. |
+/- | package-lock.json |
|
+/- | package.json |
Installed gatsby-source-filesystem and gatsby-transformer-remark plugins to source and transform markdown content. |
Part 5 - Adding GraphQL Page Query to Represent Frontmatter Data in the Index Page
In “Part 4”, we registered the Netlify CMS data to the Gatsby. However, we didn’t represent it in the UI yet. Now we’ll update the src/pages/index.js
file to list “designs” on the page. Now add the page query below to the index page to fetch the “designs” data. Now the page component will have two exports. One is for the component export, and the other one is for the page query.
// src/pages/index.js
import { graphql } from 'gatsby';
// ...
export const query = graphql`
query {
allMarkdownRemark {
edges {
node {
id
frontmatter {
title
contentKey
}
}
}
}
}
`
This query above is called “page query” in Gatsby. When we export a “graphql-tagged template” from a page, Gatsby detects this query and binds its result to the component props
in the data
key. This is how page queries work.
Gatsby offers two options to fetch data with GraphQL. One is “page query”. The other is “static query” with the
useStaticQuery
hook. (Or by the<StaticQuery>
component.) The notable difference between them is a “page query” may accept variables. (We’ll use this feature in the incoming parts.)
When querying with GraphQL, you need to add all the data nodes you’ll use explicitly to the queries. You can’t query just the
frontmatter
and get all properties it includes. You need to addfrontmatter.title
andfrontmatter.contentKey
explicitly when you build any type of GraphQL query.
Now, add a console.log(props.data)
line to the component and check the browser console if you did it right or not. You should see an object logged into the console with an allMarkdownRemark
key that holds frontmatter data from our “designs” collection.
// src/pages/index.js
const Index = (props) => {
// ...
console.log(props.data);
// ...
}
// Console Output: `Object { allMarkdownRemark: {…} }`
Another way to preview the data is to add <pre>
blocks to the index template. In some cases, it may raise a “circular dependency” error. Keep that in mind if you want to use <pre>
blocks as the debugger.
{/* src/pages/index.js */}
const Index = (props) => {
return (
<div>
{/* ... */}
<pre>{JSON.stringify(props.data, null, ' ')}</pre>
{/* ... */}
</div>
)
}
We have the GraphQL data and need to print it on the page. This part is the good old map
embed in JSX
.
{/* src/pages/index.js */}
<ul>
{
props.data.allMarkdownRemark.edges.map((edge) => {
return (
<li key={edge.node.id}>
<h1>{edge.node.frontmatter.title}</h1>
</li>
);
})
}
</ul>
The block above simply iterates over props.data.allMarkdownRemark.edges
and renders each “design” to the page. Since we have only one “design”, this will not be done much.
Now go to the http://localhost:8000/admin
route and add another design with the name “hello from design 02”. Now our loop does more and lists both items.
Let’s add a new field named “description” to the designs. Open the static/admin/config.yml
file and add a new field to the “designs” collection named “description”. Make it a “text” widget.
# static/admin/config.yml
# collections:
# - name: designs
# fields:
# - {label: "Content Key", name: "contentKey", widget: "hidden", default: "design", required: true}
# - {label: "Title", name: "title", widget: "string"}
- {label: "Description", name: "description", widget: "text"}
Reload the admin route and fill the newly added “description” fields with some description.
Now, we need to update the page query we use to fetch frontmatter data. Add “description” to the query.
Fill in at least one of the description fields in the Netlify CMS content. Otherwise, you may get an error from GraphQL, which tells you that you don’t have a
description
field.
This query will work in the GraphiQL playground, but it will throw in the development server. You need to restart the Gatsby development server for the newly added GraphQL fields.
// src/pages/index.js
export const query = graphql`
query {
allMarkdownRemark {
edges {
node {
id
frontmatter {
title
contentKey
description
}
}
}
}
}
`
After the restart, we’ll have our “description” fields in the page query result. (Check the browser console log statement again.)
Add the frontmatter.description
field right after the title
to the rendered markup and reload the page.
{/* src/pages/index.js */}
{/* ... */}
{/* <h1>{edge.node.frontmatter.title}</h1> */}
<p>{edge.node.frontmatter.description}</p>
{/* ... */}
You’ll see the new “description” field in the UI. This is the path from Netlify CMS content to the “Gatsby view layer.
Update Summary
Files Updated | Notable Changes | |
---|---|---|
+/- | src/content/designs/hello-from-design-01.md |
Updated “newly” added “description” field. |
+ | src/content/designs/hello-from-design-02.md |
Added another design example to show multiple items in index page. |
+/- | src/pages/index.js |
Added GraphQL query to fetch “designs” data and markup to list all items in a loop. |
+/- | static/admin/config.yml |
Added a “description” field to the “designs” collection. |
Part 6 - Programmatically Creating Pages from Markdown Content
Gatsby offers a couple of ways to create routes. We used the simplest one for the index page. Which is putting a JavaScript file in the src/pages
directory and exporting a React component from the file. Instead of manually adding pages, I want to add pages from the markdown content created with Netlify CMS in this part.
Before starting, remove the
src/pages/index.js
file from thepages/
directory. We don’t need it anymore since we’ll be using CMS-generated content. (After deletion, the project won’t be an index file anymore. You’ll be redirected to the default404
page of the Gatsby development server.)
Similar to the “designs” collection, I’ll add another collection for “pages”. Edit static/admin/config.yml
and add a collection item for the pages with title
, description
and body
fields. (And a contentKey
as the unique identifier for the collection.)
# static/admin/config.yml
collections:
- name: pages
label: Pages
folder: "src/pages"
create: true
fields:
- {label: "Content Key", name: "contentKey", widget: "hidden", default: "page", required: true}
- {label: "Title", name: "title", widget: "string"}
- {label: "Description", name: "description", widget: "text"}
- {label: "Body", name: "body", widget: "markdown", required: false}
Now, reload the /admin
route and create an item in this collection. Use “Index” as the title. (This will be the page’s path name.)
Previously, we sourced the src/content/
directory but not the src/pages/
to the Gatsby plugin configuration. Now add another instance of the gatsby-source-filesystem
plugin to the gatsby-config.js
file as below.
// gatsby-config.js
module.exports = {
plugins: [
// ...
{
resolve: `gatsby-source-filesystem`,
options: {
path: `${__dirname}/src/pages/`,
name: `pages`,
},
},
// ...
]
}
Restart the development server. Then, go to the GraphiQL playground, reload the page and run an allMarkdownRemark
query for all sourced items.
# GraphiQL - query
query MyQuery {
allMarkdownRemark {
edges {
node {
id
frontmatter {
title
contentKey
description
}
}
}
}
}
You’ll see that the index page content is listed along with the previously created “designs” collection items.
Now Gatsby has the page data, but the framework doesn’t know if the “page” item represents a route or not. We need to tell Gatsby that markdown files under the src/pages/
directory are site routes/pages.
There are a couple of ways to do it. There is no single correct answer. I’ll follow extending the MarkdownRemark
node types with the custom node fields method.
This part might sound a bit advanced but what we’ll have done is pretty straightforward.
Gatsby has Node APIs that offer a couple of helper methods for various operations. Two of these helpers are the onCreateNode
and createPages
APIs. As their names suggest, one is the access point to the GraphQL Node creation process. The other one is to create pages programmatically.
To use these APIs, you need to have a gatsby-node.js
file at the root of your project. Go and create one. Inside the gatsby-node.js
file, create an export.
// gatby-node.js
exports.onCreateNode = ({ node, getNode, actions }) => {
/* ... */
}
onCreateNode
helper allows developers to modify every Node created by Gatsby.
Inside the helper, we’ll filter MarkdownRemark
nodes and decorate them with a new node field. It is a slug generated by a helper imported from the source plugin gatsby-source-filesystem
. A bit confusing? Check the code block below.
// gatby-node.js
const { createFilePath } = require('gatsby-source-filesystem');
exports.onCreateNode = ({ node, getNode, actions }) => {
const { createNodeField } = actions;
if (node.internal.type === `MarkdownRemark`) {
if(node.frontmatter.contentKey === 'page') {
const slug = createFilePath({ node, getNode, basePath: `src/pages` });
createNodeField({
node,
name: `slug`,
value: slug,
});
}
}
}
This addition will, of course, need a server restart. Restart the Gatsby server and go to the GraphiQL playground. In the GraphiQL query explorer panel, check the allMarkdownRemark
query’s node
options. Along with the frontmatter
node, now we have the fields
node. This node has a slug
option. This is the slug we created programmatically in the gatsby-node.js
.
Now, add the slug
field to the query and run it. You’ll see a slug
result for the index “page” item, which is /
. You’ll see null
for the “designs” collection items.
Adding slug
fields didn’t create our routes/pages yet but gave us the path names for the “pages”. And also filtering opportunities to distinguish “pages” from other content sourced into the Gatsby. At this point, we know that non-null slug fields represent pages.
Now test the query below in the GraphiQL playground.
# GraphiQL - query
query MyQuery {
allMarkdownRemark(
filter: {
fields: {slug: {ne: null}}}
) {
edges {
node {
id
fields {
slug
}
frontmatter {
title
}
}
}
}
}
Now, you’ll see that the filtered result has only the “index” page.
Using the filter
frontmatter: {contentKey: {eq: "page"}}
would do the same since we assume the “page” key is unique. Or you may find your own solution to do filtering.
Now, we know we can create a filter for the site pages. We also have a path for them. Finally, we can create our pages programmatically with the createPages
helper. But before that, I’ll introduce the helper action createPage
, which does the actual page creation.
When we use the Gatsby Node APIs, Gatsby provides us with a collection of actions to do manipulations on the Gatsby flow. (They’re like helpers’ helpers.) One of them is the createPage
action.
The createPage
action has two required parameters, path
and component
. (We’ll use one optional parameter named context
later in the tutorial.) path
is the route path for the page, which we’ll be using our slug
field for its value. component
is the page template for the page created, which is a React component.
The optional
context
parameter is a Gatsby-controlled page context variable automatically passed to the page templates by their names. We’ll get to it in the next.
Now we know what action we’ll be using. Here is the complete createPages
helper with the createPage
action.
// gatsby-node.js
// ...
exports.createPages = async ({ graphql, actions, reporter }) => {
const { createPage } = actions;
const result = await graphql(`
{
allMarkdownRemark(
filter: {
fields: {slug: {ne: null}}
}
) {
edges {
node {
id
fields {
slug
}
}
}
}
}
`);
result.data.allMarkdownRemark.edges.forEach((edge) => {
createPage({
path: edge.node.fields.slug,
component: path.resolve(`./src/templates/default-template.js`)
});
})
}
// ...
The
graphql
query inside thegatsby-node.js
above is a helper provided by thecreatePages
helper. It acts as a page query but in afunction
form. However, it is not exported from the module here. Because we’ll use the data returned from it immediately to create pages, not in the componentprops
.
The result
object returned from the graphql
function will give us the list of our pages. We’ll call the createPage
method for each page in the result
with a path name and a template.
We’ll use the slug
fields for path names, but we haven’t got a template for the component
parameter. Now create a default template at the src/templates/
directory and name it default-template.js
.
src/templates/default-template.js
will be the fallback template for all pages. When we need to customize a page, we’ll add a new template for the custom page in the future.
// src/templates/default-template.js
import React from 'react'
const DefaultTemplate = (props) => {
console.log(props);
return (
<div>
<h1>Default Template</h1>
</div>
)
}
export default DefaultTemplate
A template file (components
) is nothing but a React component and needs to be passed to the component
parameter as an absolute path.
After adding the default template to the src/templates/
directory, restart the Gatsby development server. Then visit the home path http://localhost:8000/
. You’ll have our default template (<h1>Default Template</h1>
) as the new home now.
Now, let’s add the second page. Go to the Netlify CMS admin and create a new page using the title “About”. Then visit http://localhost:8000/about/
. We’ll have our second route /about
with the same template. No need to restart the development server this time.
When you want to list all the routes, visit the
http://localhost:8000/404/
page (Or any non-existing route.) and see the list of your possible routes. Gatsby.js development 404 page lists all routes at the end of the404
page content.
Update Summary
Files Updated | Notable Changes | |
---|---|---|
+/- | gatsby-config.js |
Sourced src/pages/ directory to the Gatsby. |
+ | gatsby-node.js |
Added to create pages programmatically from markdown files under the src/pages/ directory. |
+ | src/pages/about.md |
Included to demo a secondary page. |
- | src/pages/index.js |
Replaced src/pages/index.js . |
+ | src/pages/index.md |
Replaced by markdown file src/pages/index.md . |
+ | src/templates/default-template.js |
Fallback template for all pages. |
+/- | static/admin/config.yml |
Added “pages” collection for the markdown pages. |
Part 7 - Adding a Custom Template Selector Logic
At this point, we have an /index
and /about
pages/routes. However, we have the same content for both. The content comes from the static content of the src/templates/default-template.js
. Let’s put the default template aside and create a custom template src/templates/page-template.js
for the pages.
// src/templates/page-template.js
import React from 'react'
const PageTemplate = (props) => {
console.log(props.data);
return (
<div>
<h1>Page Template</h1>
<h2>{props.data.markdownRemark.frontmatter.title}</h2>
<p>{props.data.markdownRemark.frontmatter.description}</p>
<div
dangerouslySetInnerHTML={{
__html: props.data.markdownRemark.html
}}>
</div>
</div>
)
}
export default PageTemplate
When copying from the source code, note that I used an escape cheat to prevent a Liquid syntax error in Jekyll. So, do not forget to remove the duplicated curly braces and apostrophes if you copy and paste from the source code. (I couldn’t show the characters here because of the same error. 😄)
This template will use the title
and the description
fields from the frontmatter
as well as the html
content in the markdown file.
The default template didn’t add dynamic data to build the page content. But here, we need the frontmatter
data for the page.
Let’s get the data by adding a generic query for the pages by passing the page “id
”s to the template.
Open the page template src/templates/page-template.js
. Add a page query with markdownRemark
and pass a $pageId
variable as the query parameter.
// src/templates/page-template.js
import { graphql } from 'gatsby';
// ...
export const query = graphql`
query($pageId: String = "") {
markdownRemark(id: { eq: $pageId }) {
id
fields {
slug
}
frontmatter {
title
description
}
html
}
}
`
// ...
This page query will grab all the data we need to build up a page from the markdown data. Yet we still don’t have the $pageId
parameter. The unique id
of the current page is registered by GraphQL.
Remember the optional context
parameter from the previous part? We’ll pass the page id
s from the gatsby-node.js
file while calling the createPage
action.
// gatsby-node.js
// ...
createPage({
// ...
context: {
pageId: edge.node.id,
},
});
// ...
The code block above, added a context
parameter and passed the unique edge.node.id
as pageId
parameter. Magically Gatsby will provide this value with its name on every page created by this createPage
call.
The automatically created pages can receive context and use that as variables in their GraphQL queries.
Let’s check if the new page template is working as we expected. Go to the gatsby-node.js
and replace the component path to page-template.js
from default-template.js
.
// gatsby-node.js
createPage({
// ...
component: path.resolve(`./src/templates/page-template.js`),
// ...
});
Restart the Gatsby development server and visit the pages.
When you visit /
and /about
routes, you’ll see their markdown content is printed to the screen in place of the static default template content <h1>Default Template</h1>
.
Now we have two templates. default-template.js
is a fallback, and page-template.js
is for basic markdown pages. However, we’re switching manually between these two. Let’s change that by adding a template selecting logic.
From now on, I want to look at the templates directory. Then check if there is a custom template for the current page. To achieve this, I’ll make an assumption. From particular to general:
- Every page has a template with its slug in
-template.js
format. (i.e.about-template.js
) - If not, the page should have a template in
-template.js
format. (i.e.page-template.js
) - If not, every page uses the
default-template.js
.
// gatsby-node.js
// ...
const { accessSync, constants } = require('fs');
// ...
exports.createPages = async ({ graphql, actions, reporter }) => {
// ...
const result = await graphql(`
{
allMarkdownRemark(
# ...
) {
edges {
node {
# ...
frontmatter {
contentKey
}
}
}
}
}
`);
result.data.allMarkdownRemark.edges.forEach((edge) => {
// 1 - `-template.js`
let pageTemplatePath = path.resolve(`./src/templates/${edge.node.fields.slug.replaceAll('/', '')}-template.js`);
try {
accessSync(pageTemplatePath, constants.R_OK | constants.W_OK)
} catch (err) {
// 2 - `-template.js`
pageTemplatePath = path.resolve(`./src/templates/${edge.node.frontmatter.contentKey}-template.js`);
try {
accessSync(pageTemplatePath, constants.R_OK | constants.W_OK)
} catch (err) {
// 3 - `default-template.js`
pageTemplatePath = path.resolve(`./src/templates/default-template.js`)
}
}
createPage({
// ...
component: pageTemplatePath,
// ...
});
})
}
// ...
On the code block above, we updated the GraphQL query with the contentKey
. We’ll use it to create the template name. Then we checked the most particular template name with the slug
name. If it exists. (See the slashes /
are removed from the slug
.) If not, we checked if the template name created by the contentKey
exists. If not, we’ve fallen back to the default template.
On the createPage
action, we changed the static path with the dynamically generated template path in the conditional block.
Restart the Gatsby server after the update to apply the gatsby-node.js
changes.
Since both /
and /about
routes are pages, the template selector logic will always select the page-template.js
.
Let’s add the “designs” listing page.
Say we need a more complex template to list “designs” in a grid. We’ve added the “designs” collection in the previous parts. But we didn’t represent it anywhere on the site.
Create a new page for the “designs” in the CMS panel. Name it “Designs”. It’ll create the route /designs
and use the page-template.js
.
# src/pages/designs.md
---
contentKey: page
title: Designs
description: Designs Page Description
---
Designs Page Content with a **bold** text!
Now we have 3 pages. /
, /about
and /designs
. All are represented with the same page template.
To create a custom template for the “designs” page, strip the slashes from its slug and create a file with stripped string at the src/templates/
directory. It’ll be designs-template.js
in this case.
// src/templates/designs-template.js
import React from 'react'
const DesignsTemplate = (props) => {
console.log(props);
return (
<div>
<h1>Designs Template</h1>
</div>
)
}
export default DesignsTemplate
Restart the Gatsby development server again to apply the gatsby-node.js
changes.
Re-visit the /designs
route, and you’ll see the page-template.js
is replaced by the designs-template.js
.
You may add a
templateKey
to thefrontmatter
for this operation as in thegatsby-starter-netlify-cms
. It is all up to you.
Now, let’s customize the designs page with custom markup and page data from a GraphQL query.
// src/templates/designs-template.js
import { graphql } from 'gatsby';
import React from 'react'
const DesignsTemplate = (props) => {
console.log(props.data);
return (
<div>
<h1>Designs Page Template</h1>
<h2>{props.data.markdownRemark.frontmatter.title}</h2>
<p>{props.data.markdownRemark.frontmatter.description}</p>
<div
dangerouslySetInnerHTML={{
__html: props.data.markdownRemark.html
}}>
</div>
<ul>
{
props.data.allMarkdownRemark.edges.map((edge) => {
return (
<li key={edge.node.id}>
<h1>{edge.node.frontmatter.title}</h1>
<p>{edge.node.frontmatter.description}</p>
</li>
);
})
}
</ul>
</div>
)
}
export const query = graphql`
query($pageId: String = "") {
markdownRemark(
id: { eq: $pageId }
) {
id
fields {
slug
}
frontmatter {
title
description
}
html
}
allMarkdownRemark(
filter: {frontmatter: {contentKey: {eq: "design"}}}
) {
edges {
node {
id
frontmatter {
title
description
}
}
}
}
}
`
export default DesignsTemplate
At this point, you can have a fully customized “designs” page to list your designs however you like.
Update Summary
Files Updated | Notable Changes | |
---|---|---|
+/- | gatsby-node.js |
Added auto template selector logic and page ids are as a context variable pageId to the pages. |
+ | src/pages/designs.md |
Added listing page for the “designs”. |
+ | src/templates/designs-template.js |
Added custom template for the “designs” listing page. |
+ | src/templates/page-template.js |
Added custom template for the pages. |
Part 8 - Adding React Components to Split UI
This part will be lightweight compared to the others. We’ll add shared “Layout” and “Navigation” components to the project.
Before increasing the complexity of the project further, I think it is time to re-organize and add some reusable components to the project.
Create a Navigation.js
and Layout.js
component at src/components/
directory.
We’ll use React’s composition model and the special prop props.children
inside the main section to call specific page content into the layout.
// src/components/Navigation.js
import { Link } from 'gatsby'
import React from 'react'
const Navigation = (props) => {
return (
<nav>
<Link to='/' activeStyle={{"fontWeight": "bold"}}>/index</Link>
<Link to='/about' activeStyle={{"fontWeight": "bold"}}>/about</Link>
<Link to='/designs' activeStyle={{"fontWeight": "bold"}}>/designs</Link>
</nav>
)
}
export default Navigation
Gatsby’s built-in Link component helps with internal navigation. Add three routes /
, /about
and /designs
to the <nav />
elements to
prop.
I’ve added Link
components activeStyle
for the demo. We’ll update it with a better representation.
It is possible to create site navigation dynamically with a query in advanced scenarios. We’ll do it manually here.
// src/components/Layout.js
import React from 'react'
import Navigation from './Navigation.js'
const Layout = (props) => {
return (
<div className="Layout">
<header>
<Navigation />
</header>
<main>
{props.children}
</main>
<footer>
<Navigation />
</footer>
</div>
)
}
export default Layout
Now, update every template with the Layout
component by wrapping their markup with the <Layout>...</Layout>
tags.
I’ll use the page template as an example. Every other template requires this change to use the Layout
wrapper.
// src/templates/page-template.js
// ...
import Layout from '../components/Layout.js';
const PageTemplate = (props) => {
// ...
return (
<Layout>
{/* ... */}
</Layout>
)
}
// ...
Now we have a better composition of the UI. In the next part, we’ll add another group of pages for design details using these shared components.
Update Summary
Files Updated | Notable Changes | |
---|---|---|
+ | src/components/Layout.js |
Added a shared layout component for all templates. |
+ | src/components/Navigation.js |
Added a shared navigation component. *(No more 404 page visit for navigation.) |
+/- | src/templates/default-template.js |
Wrapped template with the shared layout. |
+/- | src/templates/designs-template.js |
Wrapped template with the shared layout. |
+/- | src/templates/page-template.js |
Wrapped template with the shared layout. |
Part 9 - Adding a Custom Template for the Design Detail Pages
createPages
helper means more control over the page creation process. Now we’ll extend our page scope to the “designs” collection items and create a page for each design.
Edit gastby-node.js
and update allMarkdownRemark
query to include “designs” collection by its contentKey
value “design”. (Notice the singular form without “s”.)
Instead of using the eq
query filter, we’ll use the in
filter. And change the filter value to an array ["page", "design"]
instead of using the string “page”.
// gatsby-node.js
// ...
const result = await graphql(`
{
allMarkdownRemark(
filter: {
frontmatter: {contentKey: {in: ["page", "design"]}},
}
) {
edges {
node {
# ...
}
}
}
}
`);
// ...
The GraphQL query above will include the “designs” collection items to the page query. I’ll also extend the onCreateNode
helper to use the same filter in the Node extension logic.
// gatsby-node.js
exports.onCreateNode = ({ node, getNode, actions }) => {
// ...
if (['page', 'design'].includes(node.frontmatter.contentKey)) {
const slug = createFilePath({ node, getNode, basePath: `src` });
// ...
}
}
The code block above does two things. It extends the node modification to the “designs” collection. Changes the basePath
to the closest parent src
instead of using the page-specific one src/pages
. Here basePath
will have no effect on path creation. You may set it to an empty string. See the note below.
Changing the
basePath
value tosrc
will have no effect on the current case. Because ingatsby-config.js
, we’ve already setpath
options of thegatsby-source-filesystem
plugin assrc/pages/
andsrc/content/
.basePath
option edits paths relative to thepath
option of thegatsby-source-filesystem
.If we were to set the
basePath
option asdesigns
ordesigns/
, it would have an effect. At that case design detail pages links were created as/hello-from-design-01/
not/designs/hello-from-design-01/
.
These changes above will give us two additional pages for the designs we’ve saved previously. Let’s update the designs listing page to add their internal links to the listing page.
Open the src/templates/designs-template.js
and extend the allMarkdownRemark
query with the slug
property.
// src/templates/designs-template.js
// ...
export const query = graphql`
# ...
query($pageId: String = "") {
allMarkdownRemark(
# filter: {frontmatter: {contentKey: {eq: "design"}}}
) {
edges {
node {
# ...
fields {
slug
}
# ...
}
}
}
}
`
export default DesignsTemplate
The query above will now include paths to the design detail pages. Now ad an internal <Link />
component to the list items and pass the slug
value to the to
prop of the Link
component.
// src/templates/designs-template.js
import { graphql, Link } from 'gatsby';
// ...
const DesignsTemplate = (props) => {
console.log(props.data);
return (
<Layout>
<div>
{/* ... */}
<ul>
{
props.data.allMarkdownRemark.edges.map((edge) => {
return (
<li key={edge.node.id}>
{/* ... */}
<Link to={edge.node.fields.slug}>{edge.node.fields.slug}</Link>
</li>
);
})
}
</ul>
</div>
</Layout>
)
}
// ...
Restart the development server and visit the /designs
route. You’ll see the links to the design detail pages printed out to the listed.
When you click the link, you’ll see the default template is used to create the page. Since we don’t have a custom template for “design” items, it’ll fall back to the default template. Go and create a design-template.js
at the src/templates/
directory with the content below. (Watch the singular form “design” without “s.”)
// src/templates/design-template.js
import { graphql } from 'gatsby';
import React from 'react'
import Layout from '../components/Layout.js';
const DesignTemplate = (props) => {
console.log(props.data);
return (
<Layout>
<div>
<h1>Design Content Template</h1>
<div>{props.data.markdownRemark.frontmatter.title}</div>
<div>{props.data.markdownRemark.frontmatter.description}</div>
</div>
</Layout>
)
}
export const query = graphql`
query($pageId: String = "") {
markdownRemark(
id: { eq: $pageId }
) {
id
frontmatter {
title
description
}
}
}
`
export default DesignTemplate
Restart the development server and revisit one of the design detail pages. You’ll see the custom template is now active with the Layout
and the Navigation
on top of the page.
Since we used the same page creation logic for “designs” and “pages” collections, the GraphQL query with the content variable
$pageId
will also work here as expected.
Update Summary
Files Updated | Notable Changes | |
---|---|---|
+/- | gatsby-node.js |
Added “designs” collection to the page creation logic. |
+ | src/templates/design-template.js |
Added a custom template for design details. |
+/- | src/templates/designs-template.js |
Updated listing page with the links to the design details. |
Chapter 2 - Summary
Chapter 2 was quite long and dusty. However, it revealed our site structure and navigation. Now we can scale the project up with new pages, custom templates and custom frontmatter data, … If you’re not bored enough, the next chapter will colourize the project a little bit more with responsive images.
Next: Chapter 3 - Working with Image Paths in the Markdown Content in Gatsby