メインコンテンツへスキップ

Gatsby で Markdown をコンテンツソースとして扱う

·6 分
Gatsby GraphQL

Gatsby は静的サイトジェネレータ(SSG)であることから、主な用途としてはブログやコーポレートサイトで利用されます。
ブログ記事などは HTML をそのまま書いたりすることはしたくないので、 Markdown からページを生成する機能を実装します。

前提と準備
#

  • Gatsby は導入済みとします。
1$ gatsby --version
2Gatsby CLI version: 3.3.0
  • シンプルなブログ機能が実装されているスターター( gatsby-starter-blog )からプロジェクトを作成しておきます。
1$ gatsby new example-website https://github.com/gatsbyjs/gatsby-starter-blog
  • パッケージマネージャーは yarn を利用します。

設定内容を見る
#

  • 作成したプロジェクトのディレクトリの下に行き、 yarn develop を実行すると、以下のような画面が立ち上がります。
    • いくつかのブログ記事も作成されています。

  • 作成されたソースは以下のようになっています。

  • 順に設定方法を見ていきます。

Markdown ファイルにメタ情報の記述:*.md
#

  • ブログ記事にする Markdown ファイルには、ファイルの冒頭に YAML 形式でメタ情報ブロック( Front-matter )を追加する必要があります。
  • 以下のような感じです:
    • 自分で項目を追加することも可能です。
1---
2title: Hello World
3date: "2015-05-01T22:12:03.284Z"
4description: "Hello World"
5---
6# 以下、本文
  • これらの情報は後に RSS フィードの追加を行うと、以下の GraphQL クエリで取得可能になります:
 1query MyQuery {
 2  allMarkdownRemark {
 3    nodes {
 4      frontmatter {
 5        date
 6        description
 7        title
 8      }
 9    }
10  }
11}

静的ファイルを扱うための設定:gatsby-config.js

  • Markdown ファイルや画像ファイルなど、静的ファイルを扱うための設定は以下のように記述されています:
 1module.exports = {
 2//...
 3  plugins: [
 4    `gatsby-plugin-image`,
 5    {
 6      resolve: `gatsby-source-filesystem`,
 7      options: {
 8        path: `${__dirname}/content/blog`,
 9        name: `blog`,
10      },
11    },
12    {
13      resolve: `gatsby-source-filesystem`,
14      options: {
15        name: `images`,
16        path: `${__dirname}/src/images`,
17      },
18    },
19//...
20  ],
21}
  • gatsby-source-filesystem のプラグインを用いることで、 Gatsby でファイルを扱えるようになります。
  • 今回は、以下の2箇所のディレクトリをプラグインで扱うところとして指定しています:
    • {$__dirname}/src/images : 画像ファイルを格納する
    • ${__dirname}/content/blog : ブログ記事本文を書く Markdown ファイルと記事内に貼り付ける画像ファイルを格納する

GraphQL のデータから RSS フィードを追加する設定:gatsby-config.js
#

  • 再び、gatsby-config.jsの以下の箇所の設定を確認します。
    • 以下の箇所で、 RSS フィードの追加を行なっています。
    • これにより、 Markdown で作成した記事のデータを GraphQL 経由で取得可能になります。
 1module.exports = {
 2//...
 3  plugins: [
 4    {
 5      resolve: `gatsby-plugin-feed`,
 6      options: {
 7        query: `
 8          {
 9            site {
10              siteMetadata {
11                title
12                description
13                siteUrl
14                site_url: siteUrl
15              }
16            }
17          }
18        `,
19        feeds: [
20          {
21            serialize: ({ query: { site, allMarkdownRemark } }) => {
22              return allMarkdownRemark.nodes.map(node => {
23                return Object.assign({}, node.frontmatter, {
24                  description: node.excerpt,
25                  date: node.frontmatter.date,
26                  url: site.siteMetadata.siteUrl + node.fields.slug,
27                  guid: site.siteMetadata.siteUrl + node.fields.slug,
28                  custom_elements: [{ "content:encoded": node.html }],
29                })
30              })
31            },
32            query: `
33              {
34                allMarkdownRemark(
35                  sort: { order: DESC, fields: [frontmatter___date] },
36                ) {
37                  nodes {
38                    excerpt
39                    html
40                    fields {
41                      slug
42                    }
43                    frontmatter {
44                      title
45                      date
46                    }
47                  }
48                }
49              }
50            `,
51            output: "/rss.xml",
52          },
53        ],
54      },
55    },
56//...
57  ],
58}

動的ページを作成するための設定:gatsby-node.js
#

  • gatsby-node.js に GraphQL を利用してページを作成する設定が記述されています。量が多いので順を追って見ていきます。
  • createPages 関数では、以下のことをしています:
    1. ブログ記事のテンプレートファイルの指定
    2. Markdown ファイルの日付順での取得
    3. Markdown ファイルがある場合は、 createPage 関数を用いてページを作成
      • createPage 関数には3つの引数を渡しています
        • path :ページの slug になる部分
        • component :適用されるテンプレート
        • context :テンプレートのコンポーネントに渡すデータ
 1//...
 2exports.createPages = async ({ graphql, actions, reporter }) => {
 3  const { createPage } = actions
 4
 5  // Define a template for blog post
 6  const blogPost = path.resolve(`./src/templates/Memo/index.tsx`)
 7
 8  // Get all markdown blog posts sorted by date
 9  const result = await graphql(
10    `
11      {
12        allMarkdownRemark(
13          sort: { fields: [frontmatter___updatedDate], order: ASC }
14          limit: 1000
15        ) {
16          nodes {
17            id
18            fields {
19              slug
20            }
21          }
22        }
23      }
24    `
25  )
26
27  if (result.errors) {
28    reporter.panicOnBuild(
29      `There was an error loading your blog posts`,
30      result.errors
31    )
32    return
33  }
34
35  const posts = result.data.allMarkdownRemark.nodes
36
37  // Create blog posts pages
38  // But only if there's at least one markdown file found at "content/blog" (defined in gatsby-config.js)
39  // `context` is available in the template as a prop and as a variable in GraphQL
40
41  if (posts.length > 0) {
42    posts.forEach((post, index) => {
43      const previousPostId = index === 0 ? null : posts[index - 1].id
44      const nextPostId = index === posts.length - 1 ? null : posts[index + 1].id
45
46      createPage({
47        path: post.fields.slug,
48        component: blogPost,
49        context: {
50          id: post.id,
51          previousPostId,
52          nextPostId,
53        },
54      })
55    })
56  }
57}
58//...
  • onCreateNode 関数は Gatsby で用意されている API で Markdown のファイル名を用いて、ページのパスの基になる slug の情報を作成します。
 1//...
 2exports.onCreateNode = ({ node, actions, getNode }) => {
 3  const { createNodeField } = actions
 4
 5  if (node.internal.type === `MarkdownRemark`) {
 6    const value = createFilePath({ node, getNode })
 7    createNodeField({
 8      name: `slug`,
 9      node,
10      value,
11    })
12  }
13}
14//...

onCreateNode 関数の動きを詳しく見てみる
#

  • onCreateNode の中身に以下のように変えて、再度ビルド(開発サーバー立ち上げ直し)してみます:
1exports.onCreateNode = ({ node, actions, getNode }) => {
2  const { createNodeField } = actions
3
4  if (node.internal.type === `MarkdownRemark`) {
5    console.log(node)
6  }
7}
  • エラーが出ますが、コンソールに以下のように出力されている箇所があります:
    • ある1つの記事の node 情報です。
 1//...
 2{
 3  id: 'b12b70e7-a1fa-50b4-b747-32bf8365a086',
 4  children: [],
 5  parent: '13cf1990-6cef-56fb-86a0-449ae4d0ac7e',
 6  internal: {
 7    content: '\n' +
 8      'Wow! I love blogging so much already.\n' +
 9      '\n' +
10      'Did you know that "despite its name, salted duck eggs can also be made from\n' +
11      'chicken eggs, though the taste and texture will be somewhat different, and the\n' +
12      'egg yolk will be less rich."?\n' +
13      '([Wikipedia Link](https://en.wikipedia.org/wiki/Salted_duck_egg))\n' +
14      '\n' +
15      "Yeah, I didn't either.\n",
16    type: 'MarkdownRemark',
17    contentDigest: '9b43b6a5553a369243bbce630e5f4461',
18    owner: 'gatsby-transformer-remark',
19    counter: 51
20  },
21  frontmatter: { title: 'My Second Post!', date: '2015-05-06T23:46:37.121Z' },
22  excerpt: '',
23  rawMarkdownBody: '\n' +
24    'Wow! I love blogging so much already.\n' +
25    '\n' +
26    'Did you know that "despite its name, salted duck eggs can also be made from\n' +
27    'chicken eggs, though the taste and texture will be somewhat different, and the\n' +
28    'egg yolk will be less rich."?\n' +
29    '([Wikipedia Link](https://en.wikipedia.org/wiki/Salted_duck_egg))\n' +
30    '\n' +
31    "Yeah, I didn't either.\n",
32  fileAbsolutePath: '/Users/tsuchinoko/example-website/content/blog/my-second-post/index.md'
33}
34//...
  • ブログの記事ページの URL は一意になれば良いので、 slug としては id を利用してもよいのですが、人間に優しくするには、 Markdown のファイル名を使います。
  • 再度、以下のようにonCreateNode の中身に以下のように変えて、ビルドしてみます:
1exports.onCreateNode = ({ node, actions, getNode }) => {
2  const { createNodeField } = actions
3
4  if (node.internal.type === `MarkdownRemark`) {
5    const fileNode = getNode(node.parent)
6    console.log(`\n`, fileNode.relativePath)
7  }
8}
  • 以下のように、 Markdown ファイルの相対パスが表示されます。
1 my-second-post/index.md
2
3 new-beginnings/index.md
4
5 hello-world/index.md
  • 拡張子 md など不必要な情報が含まれているので、createFilePath を用いて /[ファイル名]/ のようにします。
1//...
2if (node.internal.type === `MarkdownRemark`) {
3    console.log(createFilePath({ node, getNode }))
4}
5//...
  • 再度出力を確認すると以下のようになります:
    • 元々書いてあった処理と同じになります。
1/my-second-post/
2/new-beginnings/
3/hello-world/
  • Markdown の格納ディレクトリを src/data/blog などに変更した場合、親ディレクトリ名が slug には不要となるはずなので、その場合は、createFilePath({ node, getNode, basePath: [親ディレクトリ名] }) とすると、不要な箇所が消えた slug が作成できます。
  • createNodeFieldnode に追加した slug は GraphQL で確認することができます。

ページのテンプレートを確認・変更する:src/templates/blog-post.js
#

  • ページの見た目や内容に応じて変更箇所がある場合は変更します。
  • ブログページのデータ取得には以下のクエリが用いられています:
    • 変数 $id に一致する Markdown ファイルの情報を取得できます。
    • 日付順に並べた際の前の Markdown と次の Markdown の情報も取得できます。
 1  query BlogPostBySlug(
 2    $id: String!
 3    $previousPostId: String
 4    $nextPostId: String
 5  ) {
 6    site {
 7      siteMetadata {
 8        title
 9      }
10    }
11    markdownRemark(id: { eq: $id }) {
12      id
13      excerpt(pruneLength: 160)
14      html
15      frontmatter {
16        title
17        date(formatString: "MMMM DD, YYYY")
18        description
19      }
20    }
21    previous: markdownRemark(id: { eq: $previousPostId }) {
22      fields {
23        slug
24      }
25      frontmatter {
26        title
27      }
28    }
29    next: markdownRemark(id: { eq: $nextPostId }) {
30      fields {
31        slug
32      }
33      frontmatter {
34        title
35      }
36    }
37  }
  • ここまでのことが確認できれば、 http://localhost:8000/[Markdownファイル名]/ にアクセスすると、Markdown ファイルの内容が HTML に整形されて表示される仕組みが理解できるのではないかと思います。

まとめ
#

  • Gatsby で Markdown からページを動的に生成する仕組みを確認しました。
  • ブログ機能を持ったスターターを使えば、何の設定もなしに利用はできますが、自分で色々とカスタマイズするためには、この内容は理解しておかねばならないところでした。
    • このサイトでも、 Markdown ファイルはブログの記事ではなく、メモを置いておく扱いにしているので…

参考資料
#