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 the plugins array. Above, we sourced the src/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 and nodes. They are almost the same. However, edges has additional data about the node location it contains, which are the references to the next and previous nodes relative to the current Node.

Update Summary

👉 See the full diff at GitHub - Part 4.

  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 add frontmatter.title and frontmatter.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

👉 See the full diff at GitHub - Part 5.

  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 the pages/ 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 default 404 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 the gatsby-node.js above is a helper provided by the createPages helper. It acts as a page query but in a function 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 component props.

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 the 404 page content.

Update Summary

👉 See the full diff at GitHub - Part 6.

  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 ids 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:

  1. Every page has a template with its slug in -template.js format. (i.e. about-template.js)
  2. If not, the page should have a template in -template.js format. (i.e. page-template.js)
  3. 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 the frontmatter for this operation as in the gatsby-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

👉 See the full diff at GitHub - Part 7.

  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

👉 See the full diff at GitHub - Part 8.

  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 to src will have no effect on the current case. Because in gatsby-config.js, we’ve already set path options of the gatsby-source-filesystem plugin as src/pages/ and src/content/. basePath option edits paths relative to the path option of the gatsby-source-filesystem.

If we were to set the basePath option as designs or designs/, 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

👉 See the full diff at GitHub - Part 9.

  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