先日、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
"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をエクスポートするために、サイト構築プロセスで一回実行される。サイトのルートパスに配置する。
#次に投稿記事ページを生成してみる
###Reactコンポーネントで固定ページを作成
src/pages
にReactコンポーネントを作成する。
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コンポーネントに渡してレンダリングに使う記事情報を入れる変数の定義、などを書いている。
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コンポーネントに渡す。単に記事タイトルと投稿日を表示し、指定したパーマリンクで見れる記事として生成される。
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でクエリ発行する書き方の解説もあるので、これは求めていた答えなのかもと期待。
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経由で取得できるということらしい。
---
path: "posts/post1"
date: "2019年1月1日"
title: "投稿1"
---
# 投稿1
記事の内容。
---
path: "posts/post2"
date: "2019年1月2日"
title: "投稿2"
---
# 投稿2
記事の内容。
###mdファイルの記事情報をGraphQL経由で取得するコードを記述
gatsby-node.js
にはプラグインの設定、src/contents/
には記事情報のmdファイル。あとはmdファイルの記事情報をパースしたものをGraphQLで取得してページを生成する処理をgatsby-config.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ライブラリを使用しているということを意味していたらしい。
そして公式サイトの説明によると、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」を書けばいい、ということになる。
exports.createPages = ({ graphql, actions }) => {
// 全体的に省略
context: {
path: node.frontmatter.path
}
}
Gatsbyではクエリの結果をデータ取得処理関数のpropsにdata
プロパティとして返す仕様なので覚えておくこと。「dataってどこから来たの!?」と迷わないように。
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
あたりに絞ってコードの意味や相関関係を考察していく。
ここから先はテーマ全体の余分な処理の含まれたコードで解読困難なので、一旦保留として地道に調べていくことにする。とりあえずサンプルコードと、その解読途中の下書きを載せておく。
###サンプルコード
/* 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 }
});
});
};
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;
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
サンプルコードを処理ごとのセクションで分割しておいた。冒頭では通例通りモジュールを読み込んでいる。
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 }
});
});
};
一旦ここで解読保留。