8
5

More than 3 years have passed since last update.

Gatsby.js(Next.js)のテーマ制作から学ぶ【React.js × GraphQL】のブログシステムでの投稿記事情報取得

Last updated at Posted at 2020-01-02

前回の記事
【GraphQL 入門】 Next.js製ブログで記事情報を取得できるAPIを実装するための勉強ノート

先日、GraphQLを試運転するための簡易アプリを作って動作のイメージは掴めた。が、これはあくまで直接jsファイルとしてnode app.jsして動かしているだけで、next.jsに実装するにはどういったファイル構造やコーディングが必要なのかはまだ理解できていない。

どうすればすでに作りかけのNext.jsアプリにGraphQLを追加できるかを勉強しないといけない。いろいろググってみたけど、jsコードを読み解くスキル不足なので理解には到達できない。そこですでにNext.jsベースのアプリでGraphQLを実装している完成品を解剖することで実装方法を探ってみようと思う。掘り下げ式学習。

※ この記事は途中のGatsbyテーマの動作イメージまで掴めたところで執筆中断しています。一旦Next.jsのAPIを使ってみる方針に移行。

Gatsbyテーマ制作を体験してReact × GraphQLを学ぶ

参考にしたテーマは、GatsbyJS公式サイトにも公開されているgatsby-advanced-starterというテーマ。

参考記事として爆速静的サイトジェネレーターのGatsbyJs入門の解説に沿ってコードの意味を考えていく。

何がデフォルトかを把握しておくためにGatsbyJS公式ドキュメントにも頼る。

参考文献
爆速静的サイトジェネレーターのGatsbyJs入門
GatsbyJS公式ドキュメント
gatsby-advanced-starter

まずはGatsbyサイトを動かすとこまで

Gatsbyプロジェクトの生成は以下のコマンドでできる。rootPathは生成するディレクトリの指定で、なければ新規作成される。starterはGatsbyが提供するテーマのURL(GitHubのリポジトリなど)の指定。

$ gatsby new [rootPath] [starter]

採用したテーマの場合はこう。

$ gatsby new gatsby-advanced-starter https://github.com/Vagr9K/gatsby-advanced-starter

サーバ起動。

cd gatxby-advanced-starter
npm run develop

http://localhost:8000/に接続すればサイトが表示される。

起動コマンドはnpm scriptで定義されたものでもいい。

$ gatsby develop
package.json
"scipts": {
  "develop": "gatsby develop"
}

Gatsby CLIの各種コマンドの詳細を一覧表示するコマンド。

$ npx gatsby -h

Gatsbyプロジェクトのファイル構造

全て正確にではないけど、イメージを掴むのに必要な部分だけをツリー構造に表してみた。とりあえずGatsbyのコーディングを勉強していく上で全体イメージは把握しておくべきなので。

- cntents/
    - 01-01-2019.md
    - 01-02-2019.md
- data/
    - SiteConfig.js
- node_modules/
    - インストールしたnode.jsモジュール
- public/
    - 生成されたhtmlファイル群
- src/
    - favicon.png
    - components/
        - Header/
            - Header.jsx
            - Header.css
        - Footer/
            - Footer.jsx
            - Footer.css
        - SEO/
            - SEO.jsx
            - SEO.css
        - SocialLinks/
            - SocialLinks.jsx
            - SocialLinks.css
        - PostTags/
            - PostTags.jsx
            - PostTags.css
        - PostListing/
            - PostListing.jsx
            - PostListing.css
    - templates/
        - category.jsx
        - listing.jsx
        - listing.css
        - post.jsx
        - post.css
        - tag.jsx
    - pages/
        - about.jsx
        - contact.jsxなど
- static/
    - 静的ファイル(imgやtxtなど)
- gatsby-config.js
- gatsby-node.js
- jsconfig.json
- package.json
- package-lock.json

gatsby-node.jsファイルは使用するAPIをエクスポートするために、サイト構築プロセスで一回実行される。サイトのルートパスに配置する。

参考文献
The gatsby-node.js API file(公式ドキュメント)

次に投稿記事ページを生成してみる

Reactコンポーネントで固定ページを作成

src/pagesにReactコンポーネントを作成する。

src/pages/about.jsx
import React from 'react'

const About = () => (
  <div>
    <h1>About Page</h1>
    <div>This is a About Page</div>
  </div>
)

export default About

http://localhost:8000/aboutに接続するとAboutページが開く。ここまではNext.jsでやるとのと全く同じ。ポートフォリオサイトみたいな固定ページのみのサイトならこれだけで十分作れるが、ブログのような記事更新があるならそうはいかない。

createPagesというGatsby APIについて

GatsbyにはページテンプレートのReactコンポーネントからプログラマブルにページを作成するcreatePagesというAPIがある。参考にしたGatsbyテーマのファイル構造もsrc/の中にcomponents/とは別にtemplates/があるのはこういうこと。

つまり、Reactコンポーネントだけでブログサイトを構成すると記事ごとにReactコンポーネントのjsxファイルを作成するハメになり、これでは生のHTMLでサイトをコーディングしているのと何も変わらないということ。なのでReactコンポーネントを原型とした記事ページを自動生成する仕組みとしてcreatePagesが役立つ。

固定ページ用のReactコンポーネントはcomponents/に、記事ページ用のReactコンポーネントはtemplates/に置く。どちらも根っこはReactコンポーネントであることには代わりないが、パスを通す際に区別しやすくしたり、用途別に管理しやすくするために別居させているのだと思う。

個別記事情報の配列を手動で渡す例(非推奨)

とりあえずcreatePagesの仕組みを理解するために、本来ならAPIの設定を書くだけのgatsby-node.jsの中で配列を定義して、そこに手書きで入れた記事情報を渡してページがレンダリングされるだけの練習をしてみる。

サンプルコードでは、まずposts定数に記事情報の
配列を入れておき、createPagesで配列に入っている記事情報をforEachで網羅して読み込み、記事ページのパス(パーマリンク )、使用する記事ページ用のReactコンポーネントの場所、Reactコンポーネントに渡してレンダリングに使う記事情報を入れる変数の定義、などを書いている。

gatsby-node.js
const path = require("path");
const posts = [
  {
    path: "posts/1",
    date: "2019/01/01",
    title: "投稿1"
  },
  {
    path: "posts/2",
    date: "2019/01/02",
    title: "投稿2"
  }
];

exports.createPages = ({ graphql, actions }) => {
  const { createPage } = actions;
  return new Promise((resolve, reject) => {
    posts.forEach( post =>
      createPage({
        // ページのパス名
        path: post.path,
        // テンプレートとなるコンポーネントの指定
        component: path.resolve(`src/templates/post.jsx`),
        // テンプレートとなるコンポーネントに渡す変数
        context: {
          path: post.path,
          date: post.date,
          title: post.title
        }
      })
    );
    resolve();
  });
};

上記のcontextで用意した変数を、指定通りに下記のReactコンポーネントに渡す。単に記事タイトルと投稿日を表示し、指定したパーマリンクで見れる記事として生成される。

src/templates/post.jsx
import React from 'react'

const Post = ({ pageContext: { title, path, date } }) => (
  <div>
    <h1>{title}</h1>
    <div>{date}</div>
  </div>
)

export default Post

アプリを起動。

gatsby develop

そしてhttp://localhost:8000/post1に接続すれば「投稿1」という記事が表示される。createPagesが動いていることが確認できた。

満を辞してGatsbyの機能をフル活用して投稿記事ページ生成してみる

mdファイルの記事情報をGraphQL経由で自動で取得するためのプラグイン設定

この項目が最も知りたい部分。Next.jsでブログを構築したいという目的を達成するためにはReactとGraphQLの最小限のコンビネーションが手っ取り早いと考えている。しかし、GraphQLで取得する記事情報はmdファイルからなので、Gatsbyではマークダウン記法のプラグインをインストールすることになるが、そうするとGatsby用のモジュールとして動かすことになるので割とブラックボックス感があって好ましくない。その中身を知りたい。

$ npm install --save gatsby-source-filesystem gatsby-transformer-remark

gatsby-config.jsでGatsby専用プラグインをpluginsの中に列挙して読み込み設定をする。

gatsby-source-filesystem
Gatsbyからファイルシステムを読み込めるようにするもの。ここの設定の書き方からするに、多分node.jsで静的ファイルを読み込む__dirnameとかの機能をapp.useするのに近いものだと思う。resolveには導入したプラグイン名を、options.pathは適用させたいディレクトリを、options.nameは呼び出すとき用のidみたいなもの?だと思う。

gatsby-transformer-remark
マークダウンのパースをするためのもの。これもoptionsはいくつか用意されているけど、サンプルコードでは設定されてない。この辺はよくわからん。とりあえず公式ドキュメントにはGraphQLでクエリ発行する書き方の解説もあるので、これは求めていた答えなのかもと期待。

参考文献
gatsby-source-filesystem
gatsby-transformer-remark

gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        path: `${__dirname}/src/contents`,
        name: "contents",
      }
    },
    `gatsby-transformer-remark`,
  ]
}

記事情報を記述するmdファイルを作成

あとはマークダウンで記事の内容を執筆したmdファイルを、プラグインの読み込み設定で指定したsrc/contents/に作成する。gatsby-transformer-remarkによってmdファイルのFront MattterがパースされてGraphQL経由で取得できるということらしい。

src/contents/post1.md
---
path: "posts/post1"
date: "2019年1月1日"
title: "投稿1"
---

# 投稿1

記事の内容。
src/contents/post2.md
---
path: "posts/post2"
date: "2019年1月2日"
title: "投稿2"
---

# 投稿2

記事の内容。

mdファイルの記事情報をGraphQL経由で取得するコードを記述

gatsby-node.jsにはプラグインの設定、src/contents/には記事情報のmdファイル。あとはmdファイルの記事情報をパースしたものをGraphQLで取得してページを生成する処理をgatsby-config.js書けばいい。

gatsby-node.js
const path = require('path')

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

  return graphql(`
    {
      allMarkdownRemark(
        sort: {
          order: DESC,
          fields: [frontmatter___date]
        }
        limit: 1000
      ){
        edges {
          node {
            frontmatter {
              path
              date
              title
            }
          }
        }
      }
    }
  `).then(result => {
    if (result.errors) return Promise.reject(result.errors)

    result.data.allMarkdownRemark.edges.forEach(({ node }) => {
      createPage({
        path: node.frontmatter.path,
        component: path.resolve(`src/templates/blog-template.js`),
        context: {
          path: node.frontmatter.path,
          date: node.frontmatter.date,
          title: node.frontmatter.title,
        }
      })
    })
  })
}

上記のコードのreturn graphqlの部分でGraphQLでクエリ発行している。

allMarkdownRemarkの記述の部分は、公式ドキュメントgatsby-transformer-remarkの項目を見るとMarkdownRemarkノードを取得する記述とのことと書かれている。

そもそもRemarkって何やねんと思ったのでググったら「プラグインを搭載したMarkdown Processor」というキャッチコピーのRemark公式サイトを発見。MarkdownRemarkという記述はMarkdownライブラリを使用しているということを意味していたらしい。

参考文献
Remark.js公式サイト
Remark で広げる Markdown の世界

そして公式サイトの説明によると、mdファイルの記事のメタ情報を記述したfrontmatterフィールドはGraphQLフィールドに変換され、allMarkdownRemarkのノード(allMarkdownRemark.edges.node)として検出されるとのこと。投稿記事のmdファイルのfrontmatterにはメタ情報としてpath date titleの3つが書かれており、それをGraphQLフィールドに変換しているということ。

  • allMarkdownRemark.edges.node.frontmatter.path
  • allMarkdownRemark.edges.node.frontmatter.date
  • allMarkdownRemark.edges.node.frontmatter.title

つまりallMarkdownRemark(引数)の部分の意味は、マークダウンで書かれた記事を日付順にsortして1000件まで取得ということっぽい。そしてallMarkdownRemark(){取得データ}の部分は記事のメタ情報を指定している。GraphQLのデータ型やスキーマの定義などはおそらくGatsbyのプラグインの内部で済ませているのだろう。

ここで再度アプリを起動。

$ gatsby develop

そしてhttp://localhost:8000/post1に接続すると「投稿1」が表示される。

Reactコンポーネント内でGraphQLスキーマを定義

GraphQLスキーマをgatsby-node.jsで定義すると、createPagesを使わない限り記事生成のGraphQLのクエリを経由しない。これがどう問題なのかというと、憶測になるが、gatsby-node.jsというファイルは仕様上1つしか用意できないため、もし場面ごとのGraphQLクエリを使い分けたいとしたら1つのファイルに全てのクエリ定義を記述するため、可読性の低下や、どのクエリがどの用途に対応するのかが分かりにくくなる。つまりメンテナンス性に乏しい手法と言える。

そこで、GatsbyJSではReactコンポーネント内でGraphQLのクエリやスキーマを定義できるので、各記事のテンプレートとなるReactコンポーネントのファイルにまとめて「データ取得処理」「JSX」「クエリ発行」を記述する。この手法のメリットは、生成するページの内容に関する処理を全て一つのファイル内に書けるのでメンテナンスがやり易いこと。

復習がてらGraphQLで行う三大処理

  • スキーマの定義
    • データ型の定義 type [Example]
    • クエリの定義 type Query
  • データ取得処理の定義 resolvers
  • クエリ発行 graphql``

このうちスキーマの定義はGatsbyプラグインのgatsby-transformer-remarkがやってくれていると思う(予想)ので、残りの二つだけ考えればいい。そうすると、GatsbyテーマでGraphQLを使うにはReactコンポーネントで「データ取得処理」「クエリ発行」「JSX」を書けばいい、ということになる。

gatsby-node.js
exports.createPages = ({ graphql, actions }) => {
  // 全体的に省略
  context: {
    path: node.frontmatter.path
  }
}

Gatsbyではクエリの結果をデータ取得処理関数のpropsにdataプロパティとして返す仕様なので覚えておくこと。「dataってどこから来たの!?」と迷わないように。

src/templates/posts.jsx
import React from 'react'
import { graphql } from 'gatsby'

const BlogTemplate = ({ data }) => {
  const {
    markdownRemark: {
      html,
      frontmatter: {
        title,
        date
      }
    }
  } = data
  return (
    <div>
      <h1>{`${title} Page`}</h1>
      <div>date : {date}</div>
      <div dangerouslySetInnerHTML={{ __html: html }} />
    </div>
  )
}

export default BlogTemplate

export const pageQuery = graphql`
  query($path: String!) {
    markdownRemark(frontmatter: { path: { eq: $path } }) {
      html
      frontmatter {
        date
        title
      }
    }
  }
`

ここで再度アプリ起動。

$ gatsby develop

そしてhttp://localhost:8000/post1に接続すると「投稿1」のページが表示される。これにてブログの基本機能である記事情報の取得に成功した。同じようにsrc/pages/index.jsなどでGraphQLのスキーマを記述したりすれば記事一覧の取得なども可能となる。

GraphQLの記述方法 早見表

と言う感じに個人的に見分けることにしている。この辺はもっとちゃんと理解して言語化しておきたいところ。

  • typeDefsはスキーマ定義。
  • graphql``と記述されていたらクエリ発行。
  • 関数だったらデータ取得処理。

忘れかけていたけど、本題のGatsbyテーマの分析

この記事の本命の目的は「Next.js製ブログの投稿記事にカテゴリ・タグ機能を実装する」といういうことを思い出したので、すでにそれらの機能が実装されているGatsbyテーマのコードを解読していこうと思う。そもそも解読するための知識を身に付けるのがここまでの勉強だったはず。解読するGatsbyテーマは前述の通りgatsby-advanced-starterで、とりあえずはそのプロジェクトファイルのgatsby-node.js src/components/PostListing.jsx src/templates/listing.jsxあたりに絞ってコードの意味や相関関係を考察していく。

参考文献
gatsby-advanced-starter

ここから先はテーマ全体の余分な処理の含まれたコードで解読困難なので、一旦保留として地道に調べていくことにする。とりあえずサンプルコードと、その解読途中の下書きを載せておく。

サンプルコード

gatsby-node.js
/* eslint "no-console": "off"  */

const path = require("path");
const _ = require("lodash");
const moment = require("moment");
const siteConfig = require("./data/SiteConfig");

exports.onCreateNode = ({ node, actions, getNode }) => {
  const { createNodeField } = actions;
  let slug;
  if (node.internal.type === "MarkdownRemark") {
    const fileNode = getNode(node.parent);
    const parsedFilePath = path.parse(fileNode.relativePath);
    if (
      Object.prototype.hasOwnProperty.call(node, "frontmatter") &&
      Object.prototype.hasOwnProperty.call(node.frontmatter, "title")
    ) {
      slug = `/${_.kebabCase(node.frontmatter.title)}`;
    } else if (parsedFilePath.name !== "index" && parsedFilePath.dir !== "") {
      slug = `/${parsedFilePath.dir}/${parsedFilePath.name}/`;
    } else if (parsedFilePath.dir === "") {
      slug = `/${parsedFilePath.name}/`;
    } else {
      slug = `/${parsedFilePath.dir}/`;
    }

    if (Object.prototype.hasOwnProperty.call(node, "frontmatter")) {
      if (Object.prototype.hasOwnProperty.call(node.frontmatter, "slug"))
        slug = `/${_.kebabCase(node.frontmatter.slug)}`;
      if (Object.prototype.hasOwnProperty.call(node.frontmatter, "date")) {
        const date = moment(node.frontmatter.date, siteConfig.dateFromFormat);
        if (!date.isValid)
          console.warn(`WARNING: Invalid date.`, node.frontmatter);

        createNodeField({ node, name: "date", value: date.toISOString() });
      }
    }
    createNodeField({ node, name: "slug", value: slug });
  }
};

exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions;
  const postPage = path.resolve("src/templates/post.jsx");
  const tagPage = path.resolve("src/templates/tag.jsx");
  const categoryPage = path.resolve("src/templates/category.jsx");
  const listingPage = path.resolve("./src/templates/listing.jsx");

  // Get a full list of markdown posts
  const markdownQueryResult = await graphql(`
    {
      allMarkdownRemark {
        edges {
          node {
            fields {
              slug
            }
            frontmatter {
              title
              tags
              category
              date
            }
          }
        }
      }
    }
  `);

  if (markdownQueryResult.errors) {
    console.error(markdownQueryResult.errors);
    throw markdownQueryResult.errors;
  }

  const tagSet = new Set();
  const categorySet = new Set();

  const postsEdges = markdownQueryResult.data.allMarkdownRemark.edges;

  // Sort posts
  postsEdges.sort((postA, postB) => {
    const dateA = moment(
      postA.node.frontmatter.date,
      siteConfig.dateFromFormat
    );

    const dateB = moment(
      postB.node.frontmatter.date,
      siteConfig.dateFromFormat
    );

    if (dateA.isBefore(dateB)) return 1;
    if (dateB.isBefore(dateA)) return -1;

    return 0;
  });

  // Paging
  const { postsPerPage } = siteConfig;
  const pageCount = Math.ceil(postsEdges.length / postsPerPage);

  [...Array(pageCount)].forEach((_val, pageNum) => {
    createPage({
      path: pageNum === 0 ? `/` : `/${pageNum + 1}/`,
      component: listingPage,
      context: {
        limit: postsPerPage,
        skip: pageNum * postsPerPage,
        pageCount,
        currentPageNum: pageNum + 1
      }
    });
  });

  // Post page creating
  postsEdges.forEach((edge, index) => {
    // Generate a list of tags
    if (edge.node.frontmatter.tags) {
      edge.node.frontmatter.tags.forEach(tag => {
        tagSet.add(tag);
      });
    }

    // Generate a list of categories
    if (edge.node.frontmatter.category) {
      categorySet.add(edge.node.frontmatter.category);
    }

    // Create post pages
    const nextID = index + 1 < postsEdges.length ? index + 1 : 0;
    const prevID = index - 1 >= 0 ? index - 1 : postsEdges.length - 1;
    const nextEdge = postsEdges[nextID];
    const prevEdge = postsEdges[prevID];

    createPage({
      path: edge.node.fields.slug,
      component: postPage,
      context: {
        slug: edge.node.fields.slug,
        nexttitle: nextEdge.node.frontmatter.title,
        nextslug: nextEdge.node.fields.slug,
        prevtitle: prevEdge.node.frontmatter.title,
        prevslug: prevEdge.node.fields.slug
      }
    });
  });

  //  Create tag pages
  tagSet.forEach(tag => {
    createPage({
      path: `/tags/${_.kebabCase(tag)}/`,
      component: tagPage,
      context: { tag }
    });
  });

  // Create category pages
  categorySet.forEach(category => {
    createPage({
      path: `/categories/${_.kebabCase(category)}/`,
      component: categoryPage,
      context: { category }
    });
  });
};

src/components/PostListing.jsx
import React from "react";
import { Link } from "gatsby";

class PostListing extends React.Component {
  getPostList() {
    const postList = [];
    this.props.postEdges.forEach(postEdge => {
      postList.push({
        path: postEdge.node.fields.slug,
        tags: postEdge.node.frontmatter.tags,
        cover: postEdge.node.frontmatter.cover,
        title: postEdge.node.frontmatter.title,
        date: postEdge.node.fields.date,
        excerpt: postEdge.node.excerpt,
        timeToRead: postEdge.node.timeToRead
      });
    });
    return postList;
  }

  render() {
    const postList = this.getPostList();
    return (
      <div>
        {/* Your post list here. */
        postList.map(post => (
          <Link to={post.path} key={post.title}>
            <h1>{post.title}</h1>
          </Link>
        ))}
      </div>
    );
  }
}

export default PostListing;
src/templates/listing.jsx
import React from "react";
import Helmet from "react-helmet";
import { graphql, Link } from "gatsby";
import Layout from "../layout";
import PostListing from "../components/PostListing/PostListing";
import SEO from "../components/SEO/SEO";
import config from "../../data/SiteConfig";
import "./listing.css";

class Listing extends React.Component {
  renderPaging() {
    const { currentPageNum, pageCount } = this.props.pageContext;
    const prevPage = currentPageNum - 1 === 1 ? "/" : `/${currentPageNum - 1}/`;
    const nextPage = `/${currentPageNum + 1}/`;
    const isFirstPage = currentPageNum === 1;
    const isLastPage = currentPageNum === pageCount;

    return (
      <div className="paging-container">
        {!isFirstPage && <Link to={prevPage}>Previous</Link>}
        {[...Array(pageCount)].map((_val, index) => {
          const pageNum = index + 1;
          return (
            <Link
              key={`listing-page-${pageNum}`}
              to={pageNum === 1 ? "/" : `/${pageNum}/`}
            >
              {pageNum}
            </Link>
          );
        })}
        {!isLastPage && <Link to={nextPage}>Next</Link>}
      </div>
    );
  }

  render() {
    const postEdges = this.props.data.allMarkdownRemark.edges;

    return (
      <Layout>
        <div className="listing-container">
          <div className="posts-container">
            <Helmet title={config.siteTitle} />
            <SEO />
            <PostListing postEdges={postEdges} />
          </div>
          {this.renderPaging()}
        </div>
      </Layout>
    );
  }
}

export default Listing;

/* eslint no-undef: "off" */
export const listingQuery = graphql`
  query ListingQuery($skip: Int!, $limit: Int!) {
    allMarkdownRemark(
      sort: { fields: [fields___date], order: DESC }
      limit: $limit
      skip: $skip
    ) {
      edges {
        node {
          fields {
            slug
            date
          }
          excerpt
          timeToRead
          frontmatter {
            title
            tags
            cover
            date
          }
        }
      }
    }
  }
`;

gatsby-node.js

サンプルコードを処理ごとのセクションで分割しておいた。冒頭では通例通りモジュールを読み込んでいる。

gatsby-node.js
const path = require("path");
const _ = require("lodash");
const moment = require("moment");
const siteConfig = require("./data/SiteConfig");
gatsby-node.js
exports.onCreateNode = ({ node, actions, getNode }) => {
  const { createNodeField } = actions;
  let slug;
  if (node.internal.type === "MarkdownRemark") {
    const fileNode = getNode(node.parent);
    const parsedFilePath = path.parse(fileNode.relativePath);
    if (
      Object.prototype.hasOwnProperty.call(node, "frontmatter") &&
      Object.prototype.hasOwnProperty.call(node.frontmatter, "title")
    ) {
      slug = `/${_.kebabCase(node.frontmatter.title)}`;
    } else if (parsedFilePath.name !== "index" && parsedFilePath.dir !== "") {
      slug = `/${parsedFilePath.dir}/${parsedFilePath.name}/`;
    } else if (parsedFilePath.dir === "") {
      slug = `/${parsedFilePath.name}/`;
    } else {
      slug = `/${parsedFilePath.dir}/`;
    }

    if (Object.prototype.hasOwnProperty.call(node, "frontmatter")) {
      if (Object.prototype.hasOwnProperty.call(node.frontmatter, "slug"))
        slug = `/${_.kebabCase(node.frontmatter.slug)}`;
      if (Object.prototype.hasOwnProperty.call(node.frontmatter, "date")) {
        const date = moment(node.frontmatter.date, siteConfig.dateFromFormat);
        if (!date.isValid)
          console.warn(`WARNING: Invalid date.`, node.frontmatter);

        createNodeField({ node, name: "date", value: date.toISOString() });
      }
    }
    createNodeField({ node, name: "slug", value: slug });
  }
};
gatsby-node.js
exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions;
  const postPage = path.resolve("src/templates/post.jsx");
  const tagPage = path.resolve("src/templates/tag.jsx");
  const categoryPage = path.resolve("src/templates/category.jsx");
  const listingPage = path.resolve("./src/templates/listing.jsx");
gatsby-node.js
  // Get a full list of markdown posts
  const markdownQueryResult = await graphql(`
    {
      allMarkdownRemark {
        edges {
          node {
            fields {
              slug
            }
            frontmatter {
              title
              tags
              category
              date
            }
          }
        }
      }
    }
  `);
gatsby-node.js
  if (markdownQueryResult.errors) {
    console.error(markdownQueryResult.errors);
    throw markdownQueryResult.errors;
  }

  const tagSet = new Set();
  const categorySet = new Set();

  const postsEdges = markdownQueryResult.data.allMarkdownRemark.edges;
gatsby-node.js
  // Sort posts
  postsEdges.sort((postA, postB) => {
    const dateA = moment(
      postA.node.frontmatter.date,
      siteConfig.dateFromFormat
    );

    const dateB = moment(
      postB.node.frontmatter.date,
      siteConfig.dateFromFormat
    );

    if (dateA.isBefore(dateB)) return 1;
    if (dateB.isBefore(dateA)) return -1;

    return 0;
  });
gatsby-node.js
  // Paging
  const { postsPerPage } = siteConfig;
  const pageCount = Math.ceil(postsEdges.length / postsPerPage);

  [...Array(pageCount)].forEach((_val, pageNum) => {
    createPage({
      path: pageNum === 0 ? `/` : `/${pageNum + 1}/`,
      component: listingPage,
      context: {
        limit: postsPerPage,
        skip: pageNum * postsPerPage,
        pageCount,
        currentPageNum: pageNum + 1
      }
    });
  });
gatsby-node.js
  // Post page creating
  postsEdges.forEach((edge, index) => {
    // Generate a list of tags
    if (edge.node.frontmatter.tags) {
      edge.node.frontmatter.tags.forEach(tag => {
        tagSet.add(tag);
      });
    }

    // Generate a list of categories
    if (edge.node.frontmatter.category) {
      categorySet.add(edge.node.frontmatter.category);
    }

    // Create post pages
    const nextID = index + 1 < postsEdges.length ? index + 1 : 0;
    const prevID = index - 1 >= 0 ? index - 1 : postsEdges.length - 1;
    const nextEdge = postsEdges[nextID];
    const prevEdge = postsEdges[prevID];

    createPage({
      path: edge.node.fields.slug,
      component: postPage,
      context: {
        slug: edge.node.fields.slug,
        nexttitle: nextEdge.node.frontmatter.title,
        nextslug: nextEdge.node.fields.slug,
        prevtitle: prevEdge.node.frontmatter.title,
        prevslug: prevEdge.node.fields.slug
      }
    });
  });
gatsby-node.js
  //  Create tag pages
  tagSet.forEach(tag => {
    createPage({
      path: `/tags/${_.kebabCase(tag)}/`,
      component: tagPage,
      context: { tag }
    });
  });
gatsby-node.js
  // Create category pages
  categorySet.forEach(category => {
    createPage({
      path: `/categories/${_.kebabCase(category)}/`,
      component: categoryPage,
      context: { category }
    });
  });
};

一旦ここで解読保留。

次回
Next.jsのAPIルートにGraphQL置いて動かしてみた

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5