10月28日追記
Gatsbyブログについて、初noteを書きました。
【完全版】爆速GatsbyでWordPressちっくなブログを作る全手順
こんにちは、筋肉めがねです。
ドイツでは、Covid-19に関する規制が少しずつ緩和されてきており、街が活気を取り戻してきました。ですが、あまり気を緩める事なく、Post coronaの新しい形での生活スタイルを確立していけたら良いですね。
さて、このGatsbyブログですが、これまでは、トップページに全ての記事の見出しを載せる形で表示しており、とても縦に長いサイトでした。記事の数が30に到達する頃でしたので、昔の記事と最近の記事を行ったり来たりするには、上にスクロール、下にスクロールと、あまりユーザーエクスペリエンスがよろしくないサイトでした。そこで、1ページ毎に6記事の見出しを表示し、Googleの検索結果に出てくるようなページネーションを実装することで、新しい記事、古い記事へサクサクっとアクセスできるようサイトを改修しました。
記事タイトルにある通り、この記事は、「Gatsbyブログのデザインをワードプレスちっくにする手順」シリーズの、その5です。
その1からその4までの記事はこちらです。
【連載】Gatsbyブログのデザインをワードプレスちっくにする手順(その1)記事のテンプレートを作り込む。
【連載】Gatsbyブログのデザインをワードプレスちっくにする手順(その2)「この記事書いた人」っていうComponent作成とサイドバーの一部をスマホで非表示、そして背景色を変更しました。
【連載】Gatsbyブログのデザインをワードプレスちっくにする手順(その3)ヘッダーとフッターを追加しました。
【連載】Gatsbyブログのデザインをワードプレスちっくにする手順(その4)twitterシェアボタンとtwitter cardを導入しました。
それでは、早速進めていきましょう。
この記事では以下の内容について、書いていきます。
このブログでは、gatsby-starter-kitの1つである、“gatsby-v2-starter-lumen”を使っておりますが、このstarter kitでは、/src/pages直下にデフォルトでindex.jsxファイルがあります。
Gatsby公式ページの「Adding Pagination」を参考にしながら、はじめのうちは、この/src/pages/index.jsxをゴニョゴニョ変更し、試してみましたが、結局うまくいきませんでした。
その時のissueはこちらです。
Variable “$skip” of required type “Int!” was not provided
それで、結果的に動いた方法は、/src/templates/直下に”blog-list-template.jsx”というファイルを作り、そちらのページを、トップページとして読み込ませる、という事でした。
注意点とし、“blog-list-template.jsx”ファイルを作っただけでは、ページネーションは動作しません。gatsby-node.jsを更新した後に、ページネーションが正しく動作するので、それまでは、/src/pages/index.jsxは削除せずに、そのままにしておきましょう。
こちらが、blog-list-template.jsxです。基本的には、starter-kitである”gatsby-v2-starter-lumen”の/src/pages/index.jsxの内容をコピペすれば良いのですが、幾つか変更を加えております。
import React from 'react'
import Helmet from 'react-helmet'
import { graphql } from 'gatsby'
import Layout from '../components/Layout'
import Post from '../components/Post'
import Sidebar from '../components/Sidebar'
import { Link } from 'gatsby'
import { blue } from 'color-name'
class BlogList extends React.Component {
render() {
const items = []
const { title, subtitle } = this.props.data.site.siteMetadata
const posts = this.props.data.allMarkdownRemark.edges
// 以下のcurrentとtotalがとても大事です。
// currentは、現在何ページ目にいるか、という事ですが、ブログのトップページであればcurrentは1であり、全部で5ページある、という事であれば、totalは5となります。
const {
current,
total,
} = this.props.pageContext
// ここで、itemsというArrayに、1記事1記事の情報を格納しております。
posts.forEach(post => {
items.push(<Post data={post} key={post.node.fields.slug} />)
})
return (
<Layout>
<div>
<Helmet>
<title>{title}</title>
<meta name="description" content={subtitle} />
</Helmet>
{/* commented out by kinnniku <Sidebar {...this.props} /> */}
<div className="content">
<div className="content__inner">{items}</div>
{/* これ以降のul, liでページネーションを表示しています。 */}
<ul style={{
display: `flex`,
flexWrap: `wrap`,
justifyContent: `center`,
listStyle: `none`,
paddingLeft: 30,
paddingRight: 30,
margin: `0 auto`,
}}
className="pagination">
<li className={current === 1 ? 'disabled' : ''}>
<Link to={'./'}>1</Link>
</li>
<div className={current === total || current === total - 1 ? '' : 'disabled'}>
...
</div>
<li className={current === 1 || current === 2 ? 'disabled' : ''}>
<Link to={`/page/${current -1}`}>{current-1}</Link>
</li>
<li style={{backgroundColor:`#5d93ff`}}>
<Link style={{color:`white`}} to={`/page/${current}`}>{current}</Link>
</li>
<li className={current === total || current === total - 1 ? 'disabled' : ''}>
<Link to={`/page/${current +1}`}>{current+1}</Link>
</li>
<div className={current === 1 || current === 2 ? '' : 'disabled'}>
...
</div>
<li className={current === total ? 'disabled' : ''}>
<Link to={`/page/${total}`}>{total}</Link>
</li>
</ul>
</div>
<Sidebar {...this.props} />
</div>
</Layout>
)
}
}
export default BlogList
export const pageQuery = graphql`
# gatsbyの公式ページにある通り、$skip: Int!, $limit: Int!を設定します。
query blogListQuery($skip: Int!, $limit: Int!){
site {
siteMetadata {
title
subtitle
copyright
menu {
label
path
}
author {
name
twitter
github
}
}
}
allMarkdownRemark(
#デフォルとでは、limit: 1000になっているので、それを$limitに変更します。
limit: $limit
filter: { frontmatter: { layout: { eq: "post" }, draft: { ne: true } } , fields: { draft: { eq: false } } }
sort: { order: DESC, fields: [frontmatter___date] }
# skip: $skipを追記します。
skip: $skip
) {
edges {
node {
fields {
slug
categorySlug
}
frontmatter {
title
featuredImage {
childImageSharp {
sizes(maxWidth: 630) {
...GatsbyImageSharpSizes
}
}
}
path
date
category
description
}
}
}
}
}
`
これで、blog-list-template.jsxの設定が終わりました。続いて、gatsby-node.jsです。
変更したgatsby-node.jsは以下のとおりです。サイトが読み込まれる時に、gatsby-node.jsが読み込まれ、その中でcreatePageというfunctionが呼ばれ、ページが作られていく、という仕組みです。
gatsby-node.jsの変更箇所については、コード内にコメントしておりますが、特に大事な箇所については、コードの下に、注意点として書いておきます。
const _ = require('lodash')
const Promise = require('bluebird')
const path = require('path')
const slash = require('slash')
exports.createPages = ({ graphql, actions }) => {
const { createPage } = actions
return new Promise((resolve, reject) => {
const postTemplate = path.resolve('./src/templates/post-template.jsx')
const pageTemplate = path.resolve('./src/templates/page-template.jsx')
const tagTemplate = path.resolve('./src/templates/tag-template.jsx')
// ここで先ほど作成したblog-list-template.jsxを読み込んで、blogPostListという変数に入れます。
const blogPostList = path.resolve('./src/templates/blog-list-template.jsx')
const categoryTemplate = path.resolve('./src/templates/category-template.jsx')
graphql(`
{
allMarkdownRemark(
# ここのlimitはデフォルトの1000で問題ありません。
limit: 1000
filter: { frontmatter: { draft: { ne: true } } , fields: { draft: { eq: false } } }
# 注意点1(説明はコード下の説明文を参照) sortは大事。
sort: { order: DESC, fields: [frontmatter___date] }
) {
edges {
node {
fields {
slug
}
frontmatter {
#デフォルトでtitleが入っていなければ、titleを追加しましょう。
title
tags
layout
category
}
}
}
}
}
`).then(result => {
if (result.errors) {
console.log(result.errors)
reject(result.errors)
}
const posts = result.data.allMarkdownRemark.edges
// 注意点2(説明はコード下の説明文を参照) 新たにcountという変数を定義。
let count = 0
posts.forEach((post) => {
if(post.node.fields.slug.includes('/posts')){
count = count + 1
} else{
}
})
// このpostsPerPageで、1ページに何記事の見出しをリストアップするのか、設定します。
const postsPerPage = 6
// 注意点3(説明はコード下の説明文を参照) 記事数、そして1ページ毎の記事数(ここでは6)から、全体のページ数を算出します。
let numPages = Math.ceil(count / postsPerPage)
// ここで、複数のページを作ります。各ページ毎に6記事の見出しがリストアップされます。
Array.from({ length: numPages }).forEach((_, index) => {
const withPrefix = pageNumber => pageNumber === 1 ? `/` : `/page/${pageNumber}`
const pageNumber = index + 1
createPage({
path: withPrefix(pageNumber),
// 上で作成したblogPostList変数を使用します。
component: blogPostList,
context: {
limit: postsPerPage,
skip: index * postsPerPage,
current: pageNumber,
total: numPages,
hasNext: pageNumber < numPages,
nextPath: withPrefix(pageNumber + 1),
hasPrev: index > 0,
prevPath: withPrefix(pageNumber - 1),
},
})
})
//ここで1記事1記事、記事毎のページを作ります。
let postsBlog = []
posts.forEach((post) => {
// /postsというフィルターをかけることで、ブログの記事に対してのみ、ページを作成します。
// 例えば、このブログには、profileというページがありますが、ここでは、profile, portfolioといった「ページ」は作成されません。
if(post.node.fields.slug.includes('/posts')){
postsBlog.push(post)
} else{
}
})
postsBlog.forEach((post, index) => {
const previous = index === postsBlog.length - 1 ? null : postsBlog[index + 1].node
const next = index === 0 ? null : postsBlog[index - 1].node
// 実際に各記事のページを作成しているのは、以下のfunctionです。
createPage({
path: `${post.node.fields.slug}`,
component: postTemplate,
context: {
slug: post.node.fields.slug,
previous,
next,
},
})
})
_.each(result.data.allMarkdownRemark.edges, edge => {
// 上で、各記事ごとのページを作成したので、残りは、profile, portfolioなどの「ページ」を作成します。
if (_.get(edge, 'node.frontmatter.layout') === 'page') {
createPage({
path: edge.node.fields.slug,
component: slash(pageTemplate),
context: { slug: edge.node.fields.slug },
})
} else if (_.get(edge, 'node.frontmatter.layout') === 'post') {
{/* ここは、starter-kitにデフォルトで入っている箇所ですが、既に、各記事ごとのページは作成したので、この部分はコメントアウトで無効にしておきます。
createPage({
path: edge.node.fields.slug,
component: slash(postTemplate),
context: {
slug: edge.node.fields.slug,
},
})
*/}
// 以下は、タグ毎、カテゴリ毎のページ作成ですね。デフォルトのままです。
let tags = []
if (_.get(edge, 'node.frontmatter.tags')) {
tags = tags.concat(edge.node.frontmatter.tags)
}
tags = _.uniq(tags)
_.each(tags, tag => {
const tagPath = `/tags/${_.kebabCase(tag)}/`
createPage({
path: tagPath,
component: tagTemplate,
context: { tag },
})
})
let categories = []
if (_.get(edge, 'node.frontmatter.category')) {
categories = categories.concat(edge.node.frontmatter.category)
}
categories = _.uniq(categories)
_.each(categories, category => {
const categoryPath = `/categories/${_.kebabCase(category)}/`
createPage({
path: categoryPath,
component: categoryTemplate,
context: { category },
})
})
}
})
resolve()
})
})
}
// これ以降もデフォルトのままです。
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions
if (node.internal.type === 'File') {
const parsedFilePath = path.parse(node.absolutePath)
const slug = `/${parsedFilePath.dir.split('---')[1]}/`
createNodeField({ node, name: 'slug', value: slug })
} else if (
node.internal.type === 'MarkdownRemark' &&
typeof node.slug === 'undefined'
) {
const fileNode = getNode(node.parent)
let slug = fileNode.fields.slug
if (typeof node.frontmatter.path !== 'undefined') {
slug = node.frontmatter.path
}
createNodeField({
node,
name: 'slug',
value: slug,
})
if (node.frontmatter.tags) {
const tagSlugs = node.frontmatter.tags.map(
tag => `/tags/${_.kebabCase(tag)}/`
)
createNodeField({ node, name: 'tagSlugs', value: tagSlugs })
}
if (typeof node.frontmatter.category !== 'undefined') {
const categorySlug = `/categories/${_.kebabCase(
node.frontmatter.category
)}/`
createNodeField({ node, name: 'categorySlug', value: categorySlug })
}
}
}
この時点で、トップページでは、全ての記事ではなくて、最近の6記事のみの見出しが表示されているはずです。/src/pages/index.jsxを無効(削除)にし(もちろん、バックアップを取ることを忘れずに)、gatsby developで確認しましょう。
gatsby clean
gatsby develop
ちなにみ、gatsbyのローカル環境に.cacheフォルダや、publicフォルダがあると、変更が正常に反映されないことがあるので、gatsby cleanで、.cacheフォルダ、publicフォルダを消し、gatsby developで開発環境をたちあげましょう。
これで、サイトのトップページから2ページ目、3ページ目へと遷移できるはずです。
問題なく動作できている事を確認できたら、/src/pages/index.jsxは消しましょう。
最後です。各記事のページにおいて、「新しい記事」、「古い記事」というリンクを貼ります。
import React from 'react'
import { Link } from 'gatsby'
import moment from 'moment'
...
class PostTemplateDetails extends React.Component {
render() {
const { author } = this.props.data.site.siteMetadata
const post = this.props.data.markdownRemark
const htmlAst = post.htmlAst
const featuredImgSize = post.frontmatter.featuredImage.childImageSharp.sizes.src
const { tags } = post.frontmatter;
const path = post.frontmatter;
const postpath = `https://kinnikumegane.com/${path}`
const { description: postDescription } = post.frontmatter
const image = `https://kinnikumegane.com/${featuredImgSize}`
// ここで、gatsby-node.jsで設定したprevious, nextを読み込みます。
const { previous, next } = this.props.pageContext
const tagsBlock = (
<div className="post-single__tags">
<ul className="post-single__tags-list">
{tags &&
tags.map((tag, i) => (
<li className="post-single__tags-list-item" key={tag}>
<Link to={tag} className="post-single__tags-list-item-link">
{post.frontmatter.tags[i]}
</Link>
</li>
))}
</ul>
</div>
)
return (
<div>
<Helmet>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={post.frontmatter.title} />
<meta name="twitter:image" content={image} />
<meta name="twitter:description" content={postDescription} />
<meta name="twitter:site" content="@KinnikuMeganeDe" />
<meta property="og:title" content={post.frontmatter.title} />
<meta property="og:type" content="article" />
<meta property="og:image" content={image} />
<meta property="og:url" content={postpath} />
<meta property="og:description" content={postDescription} />
</Helmet>
<div className="post-single">
<div className="post-single__inner">
<h1 className="post-single__title">{post.frontmatter.title}</h1>
<div className="post-single__date">
{moment(post.frontmatter.date).format('YYYY年M月DD日')}
</div>
<div className="post-single__body">{renderAst(htmlAst)}</div>
</div>
<div className="post-single__footer">
<h3 className="post-single__footer-sharesuru">シェアする</h3>
{/* add SNS button*/}
<a className="twitter-share-button" data-size="large" href="https://twitter.com/share?ref_src=twsrc%5Etfw" data-show-count="false">Tweet</a>
<ul
style={{
display: `flex`,
flexWrap: `wrap`,
justifyContent: `space-between`,
listStyle: `none`,
padding: 0,
marginTop: 60,
}}
>
{/* 一番最新の記事であれば、「新しい記事」というリンクは表示しません。 */}
<li className={next ? '' : 'disabled'}>
{next&& (
<Link to={next.fields.slug} rel="next">
← 新しい記事
</Link>
)}
</li>
{/* 一番古い記事であれば、「古い記事」というリンクは表示しません。 */}
<li className={previous ? '' : 'disabled'}>
{previous && (
<Link to={previous.fields.slug} rel="prev">
古い記事 →
</Link>
)}
</li>
</ul>
<hr />
...
</div>
</div>
<Sidebar {...this.props} />
</div>
)
}
}
export default PostTemplateDetails
「新しい記事」、「古い記事」のリンク設定はこれで完了です。
ページネーションを追加する上で、以下のページがとても参考になりました。ありがとうございました。
ブログをMiddlemanからGatsbyに乗り換えた雑感
Adding Pagination
Adding a List of Markdown Blog Posts
この記事では、以下の内容について書きました。
ページネーションを設定する事で、サイト内でのページ移動が、より簡単になったのではないかと思います。それでは、本日は以上です。