本文書は公開済みの”基礎から始めるGatsbyJS入門”の続きです。前回の文書ではGraphQLのqueryを利用してMarkdownファイルの内容を取得しブラウザに表示できることまでを確認しました。

本文書では一歩進みMarkdownファイル毎に個別ページのslug(URL or PATH)を動的に作成しそのslug(URL or PATH)にアクセスするとMarkdownファイルの内容が動的に表示できるコードを作成していきます。

¥src¥pagesの下にpostsディレクトリを作成し, first-article.md, second-article.mdの2つのファイルが保存されている状態から初めて動的なページの作成方法を確認していきます。これまでの記事を読んでおくことは必須ではありません。

slug(URL or PATH)の取得、ページの作成するために見慣れないGatsbyの関数(API)が出てくるのでJavascript、プログラミングの入門者の方は少し難しく感じるかもしれませんができるだけ詳細に説明しています。

slugを作成する方法はいくつもあるので本文書はその一例です。MarkdownファイルではなくContentfullなどのHeadless CMSを利用する場合はslugの作成方法が異なります。またabout.jsファイルのような静的ページのようにpagesディレクトリの下に記事ファイルを作成していくことでも記事とURLを紐づけること可能です。しかしここではその方法は利用せず、Markdownファイルのファイル名とパスを利用して動的にページの作成を行なっていきます。
fukidashi

Slug(URL or PATH)の作成

slugについて

ブラウザからアクセスするURLやPATHを以下ではslugという名称を利用します。

本文書ではpostsディレクトリに保存したMarkdownファイルのパスを利用してslugを作成します。slugはMarkdownファイルのファイル名だけを利用して作成することもできます。

最終的に下記のように表示されているブログ一覧のタイトルをクリックすると各ブログの記事の内容が閲覧できるように設定を行なっていきます。

 ブログタイトル一覧
ブログタイトル一覧

URLが少し見えにくいかもしれませんが、URLにはhttp://localhost:8000/posts/first-article/を設定しています。/posts/first-articleにアクセス後に表示される内容はファイルパスであるposts¥first-article.mdの内容に対応します。

ブログページの確認
ブログページの確認

gatsby-node.jsファイル

動的にslugとページを作成するためにnodeの設定ファイルであるgatsby-node.jsファイルをインストールディレクトリに作成します。デフォルトではこのファイルは存在しないので手動で作成する必要があります。gatsby-node.jsに記述したコードはビルド(gatsby develop)する際に実行されます。

slugの作成

本文書でのslugはmdファイルのファイルパスと名前から作成します。postsディレクトリのfirst-article.mdであればslugは/posts/first-articleとなります。

slugを作成するためにはslugの名前である/posts/first-articleをGatsby内の情報から取得する必要があります。直接/posts/first-articleと取得することができないため、これから説明を行う処理を行うことで/posts/first-articleを取得します。

/posts/fist-article/という情報が取得できたらnodeに取得したslugを追加します。追加が完了するとGraphQLのqueryからがslugを取得できるようになります。。

nodeに追加したslugはただの/posts/first-articleという文字列にすぎないためslugを作成しただけではページは作成できません。そのためslugを作成した後にページを作成するための処理を追加する必要があります。

Gatsbyサイトを構成するフォルダやファイルなどすべてnodeです。
fukidashi

slugの情報を作成するためにGatsbyのAPIであるonCreateNodeを利用します。onCreateNodeを利用してMarkdownファイルのパス名を取り出してslugの作成に使います。

MarkdownファイルのnodeはGraphQLのquery(allMarkdownRemark)で取得することができます。nodeにはファイルの名前やパスなどの情報が含まれています。このnodeの中に新たにslugを情報として追加してます。
fukidashi

slugを作成するためはMarkdownファイルの情報を取得する必要があります。Markdownファイルに関連するはnodeがslugを作成するための必要なデータを持っています。Markdownファイルのnode情報のみ取得するためnodeのデータ構造の一つであるinternalのtypeを利用します。

nodeのinternal typeを利用するのはnodeのデータ構造が下記のように決められておりMarkdownファイルの場合はtypeにMarkdownRemarkが設定されるためです。

Node Data Structure
Node Data Structure
ページファイルのabout.jsファイルなどを含めGatsby上のすべてのnodeがtypeを持ちます。
fukidashi

typeにはプラグインのOwnerによって選択されグローバルでユニークの名前がつけられています。MarkdownファイルのOwnerはインストールしたgatsby-transformer-remarkです。MarkdownファイルのtypeはMarkdownRemarkに設定されているためtypeにMarkdownRemarkを指定することでnodeの中からMarkdownファイルのnode情報のみ取得することが可能となります。

実際にgatsby-node.jsファイルに以下のコードを追加して、typeにMarkdownRemakを持つnodeを取得します。


exports.onCreateNode = ({ node }) => {
  if (node.internal.type === `MarkdownRemark`) {
    console.log(node)
  }
}

設定後にgatsby developを再実行するとビルドのメッセージの中にMarkDownRemarkのtypeを持つnodeの情報が表示されます。


// Result command:gatsby develop
//略
{
  id: 'd9feff1d-fcea-5d55-9fdf-fdb773dc50b8',
  children: [],
  parent: '8c925fee-e2b5-5dd6-b3c9-91cc3f3f9193',
  internal: {
    content: '\n### Gatsby での最初の記事\n\nGatsby サイト上に表示するための初めての記事です。\n\nブラウザ上に表示されるのかの確認です。\n',
    type: 'MarkdownRemark',
    contentDigest: 'af41eff8bc81a0ba45d5106599ad5aa9',
    counter: 34,
    owner: 'gatsby-transformer-remark'
  },
  frontmatter: { title: 'はじめての記事', date: '2020-08-18' },
  excerpt: '',
  rawMarkdownBody: '\n### Gatsby での最初の記事\n\nGatsby サイト上に表示するための初めての記事です。\n\nブラウザ上に表示されるのかの確認です。\n',
  fileAbsolutePath: '/Users/mac/Desktop/gatsby-site/src/posts/first-article.md'
}
//略
上記のResultに表示しているnode情報はgatsby developを実行後のメッセージの中に表示されますが、その他のたくさんの情報も一緒に表示されます。ここではtypeにMarkdownRemakを持つnodeの情報のみ取り出しています。
fukidashi

取得したMarkdownファイルの情報からMarkdownファイルのパスを取得してslugを作成します。fileAbsolutePathからfirst-articleという名前を取り出してslugにすることも可能ですが、Gatsbyのドキュメントを参考にgatsby-source-filesystemプラグインのcreateFilePathを利用して/posts/first-articleという名前を取り出します。

gatsby-source-filysystemのcreateFilePathの利用は必須ではありません。
fukidashi

以下のコードを利用してMarkdownファイルのrelativePath(相対パス)の情報を取得します。Markdownファイルのnode自体にはrelativePathを持っていない(先ほどのconsole.log(node)の結果を参照)のでnode.parentにgetNodeメソッドでアクセスしてfileNodeの情報を取得し、そこからrelativePathの情報を取得します。


exports.onCreateNode = ({ node, getNode }) => {
  if (node.internal.type === `MarkdownRemark`) {
    const fileNode = getNode(node.parent)
    console.log(`\n`, fileNode.relativePath)
  }
}

gatsby developコマンドを再実行するとそのメッセージの中にMarkdownファイルのrelativePathの情報が表示されます。


//Result command: gatsby develop
//略
 pages/posts/second-article.md

 pages/posts/first-article.md
//略

拡張子mdなど不必要な情報が含まれているので、posts/first-article.mdをcreateFilePathを利用して/posts/first-article/の形に変更します。


const { createFilePath } = require(`gatsby-source-filesystem`)
exports.onCreateNode = ({ node, getNode }) => {
  if (node.internal.type === `MarkdownRemark`) {
    console.log(createFilePath({ node, getNode, basePath: `pages` }))
  }
}

これでslugの作成は完了です。


//Result command: gatsby develop
/posts/first-article/
/posts/second-article/

取り出したslugをnodeに追加を行うためcreateNodeFieldを利用します。


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: `pages` })
    createNodeField({
      node,
      name: `slug`,
      value: slug,
    })
  }
}

コードの内容はMarkdownファイルのパスを取り出しslugを作成、その後、crateNodeFieldのnameにslugを設定しその値には作成したslugを設定しています。

createNodeFieldでnodeに追加したslugはGraphiQLで確認することができます。GraphiQLのExplorerで確認するとnodeに新たにfieldが追加されその下にslugも追加されます。追加されていない場合はgatsby developコマンドの再実行を忘れている場合があるのでその場合は実行を行ってください。

nodeにslugの追加確認
nodeにslugの追加確認

ここまででslugの作成は完了です。次はこのslugにブラウザでアクセスした際にMarkdownファイルの内容が表示されるようにページの作成を行います。

動的ページの作成

templateファイルの作成

ブラウザから/posts/first-article/にアクセスした際にMarkdownファイルの内容を表示させる必要がありますがその前にtemplateファイルを作成します。templateファイルによってページのフォーマットが決まります。一般的にブログなどの記事はページによってページフォーマットを変える必要がないため、1つのtemplateファイルをすべての記事で共有します。templateファイルが作成できれば取得したデータを決められた場所に配置することで同じフォーマットの記事が表示されます。

srcディレクトリの下にtemplatesディレクトリを作成しさらにその下にpost.jsファイルを作成します。内容は下記の通りでこの時点では”ブログ記事のコンテンツ”と表示されるだけです。この時点では、Markdownファイルから内容を取得し表示させるといった設定は行っていません。


import React from "react"
import Layout from "../components/layout"

export default function Post() {
  return (
    <Layout>
      <div>ブログ記事のコンテンツ</div>
    </Layout>
  )
}

createPageについて

ページを動的に作成する場合は、GatsbyのAPIのcreatePageを利用します。cratePageの構造は下記の通りでシンプルです。


createPage({
  //ページのパス設定
  path: node.fields.slug,
  //ページテンプレートのパス設定
  component: path.resolve(`./src/templates/post.js`),
  //ページテンプレートに渡したいデータの設定
  context: {
    // Data passed to context is available
    // in page queries as GraphQL variables.
    slug: node.fields.slug,
  },

動的にページを作成するために利用するcreatePageの構造がわかったのでgatsby-node.jsファイルに動的ページ作成のコードを記述します。

動的ページの作成コード

前半のcreateNodeFiledへのslugについては説明済で前半のコードでMarkdownファイルのnodeにslugを追加していいます。exports.createPagesからが追加したコードになります。先頭にJavaScriptのpathモジュールをrequireしているので忘れないようにしてください。

前半のコードでslugはnodeに追加されるためGraphQLでnodeから追加したslugを取得しています。GraphQLを実行すると複数のMarkdownファイルがあるので複数のnodeが取得できます。取得したresultをforEachすることで各nodeのslugを取り出してcreatePageの引数に入れてページの作成を行っています。


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: `pages` })
    createNodeField({
      node,
      name: `slug`,
      value: slug,
    })
  }
}
exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions
  const result = await graphql(`
    query {
      allMarkdownRemark {
        edges {
          node {
            fields {
              slug
            }
          }
        }
      }
    }
  `)
  result.data.allMarkdownRemark.edges.forEach(({ node }) => {
    createPage({
      path: node.fields.slug,
      component: path.resolve(`./src/templates/post.js`),
      context: {
        slug: node.fields.slug,
      },
    })
  })
}

gatsby-node.jsを保存したらgatsby developコマンドを再実行して変更を反映させます。

ブラウザからslugの/post/first-article/にアクセスして以下の内容が表示されたら動的なページ作成は成功しています。

slugにアクセスするとページが閲覧できる
slugにアクセスするとページが閲覧できる

templateファイルpost.jsに”ブログ記事のコンテンツ”と記述しているので/posts/second-article/とアクセスしても同じ内容になります。

Markdownファイルからの内容取得

Markdownファイルに記述したファイルの内容を表示させるためにpost.js内でGraphQLのqueryを実行してファイルの内容を取得する必要があります。どのファイルから取得するかはcreatePageの引数で設定したslugで渡された値を利用します。以下のqueryでは変数$slugに一致したMarkdownファイルの内容を取得することができます。


export const query = graphql`
  query($slug: String!) {
    markdownRemark(fields: { slug: { eq: $slug } }) {
      html
      frontmatter {
        title
      }
    }
  }
`

このGraphQLのqueryをtemplateファイルに追加して、取得した内容でページの内容を表示させます。htmlの部分はdangerouslySetInnerHTMLを利用しています。


import React from "react"
import { graphql } from "gatsby"
import Layout from "../components/layout"

export default function Post({ data }) {
  return (
    <Layout>
      <h1>{data.markdownRemark.frontmatter.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: data.markdownRemark.html }} />
    </Layout>
  )
}
export const query = graphql`
  query($slug: String!) {
    markdownRemark(fields: { slug: { eq: $slug } }) {
      html
      frontmatter {
        title
      }
    }
  }
`

ブラウザで再度/posts/first-article/にアクセスするとfirst-article.mdファイルの中身が表示されます。

mdファイルの内容が表示
mdファイルの内容が表示

/posts/second-artcile/にアクセスするとsecond-article.mdファイルの内容が表示されることも確認きます。この後にthird-article.mdファイルを作成しても/posts/third-article.mdでアクセスすることが可能です。

リンクの設定

トップページのindex.jsファイルに表示されているブログのタイトルから各ページに移動できるようにLinkの設定を行います。

slugが取得できるようにqueryの中にslugを追加します。追加したslugはLinkタグを使って各ページへのリンクを貼ります。


import React from "react"
import Layout from "../components/layout"
import { graphql, Link } from "gatsby"

export default function Home({ data }) {
  return (
    <div>
      <Layout>
        <h1>Gatsby Blog Site</h1>
        {data.allMarkdownRemark.nodes.map(node => (
          <div key={node.id}>
            <Link to={node.fields.slug}>
              <h2>{node.frontmatter.title}</h2>
            </Link>
            <p>{node.frontmatter.date}</p>
          </div>
        ))}
      </Layout>
    </div>
  )
}

export const query = graphql`
  {
    allMarkdownRemark {
      nodes {
        html
        fields {
          slug
        }
        frontmatter {
          date
          title
        }
      }
    }
  }
`

トップページのブログ一覧にリンクが貼られ、クリックすると各ページにアクセスすることができます。

トップページのリストへのリンク設定
トップページのリストへのリンク設定

動的ページの作成とブログ記事の一覧からのリンクを設定することができました。Markdownファイルを使って記事を追加していけば動的にページが作成される仕組みができました。文字だけのブログであればここまでの知識でブログを構築することも可能です。

しかし、ブログの記事には画像を追加したいものです。

次はGatsbyで画像を扱えるように設定を行なっていきます。