ブログでよくある年月別アーカイブをGatsbyで実装しました。以下のよくある構成です。
/archives/
: 年月別アーカイブ一覧/archives/YYYY/
: 年別記事一覧/archives/YYYY/MM/
: 月別記事一覧/archives/YYYY/
と/archives/YYYY/MM
で該当する年、月ごとの記事一覧ページをgatsby-node.jsで生成する必要があります。これは以下を参考にしました。
参考: Gatsby.jsで年ごと、月ごとで記事一覧を表示したい - Qiita
Setオブジェクトでまとめることで重複なしの年月一覧リストが作れるところがミソっぽいです。
記事ごとの前後リンクは既に実装しているので、アーカイブでも2020年2月のページから2020年1月や2020年3月に移動できたり、2018年から2017年や2019年に時系列的な前後関係に移動できると便利そうです。このブログは空白の期間だらけなので、単純に翌月前月ではなく記事が存在する最も近い月を情報として持っておく必要があるので、gatsby-node.jsでページを生成するときにcontextで渡してあげる必要があります。
years
、yearMonths
というSetオブジェクトに記事が存在する年、年月が入っています。Setは挿入順に反復することができるので、通常の配列と同じように次の要素も引っ張ってこれそうな雰囲気があったのですが、あくまでも順序を持たないため、ループ処理中に次の値を引っ張ってくる方法がどうもないっぽいのです。というわけで、Array.from()メソッドで配列を作る必要があります。配列であれば各要素にindexでアクセスできるので、ループ中に次の要素にアクセスできるので、これをcontextで渡すようにします。
// 年別ページ
const yearList = Array.from(years)
let prevYear;
yearList.forEach((year, index) => {
const nextYear = yearList[index + 1];
createPage({
path: `/archives/${year}/`,
component: archiveTemplate,
context: {
thisYear: year,
prevPage: prevYear,
nextPage: nextYear,
periodStartDate: `${year}-01-01T00:00:00.000Z`,
periodEndDate: `${year}-12-31T23:59:59.999Z`
}
})
prevYear = year
})
月別も同じようにします。
// 月別ページ
const yearMonthList = Array.from(yearMonths);
let prevYearMonth;
yearMonthList.forEach((yearMonth, index) => {
const [year, month] = yearMonth.split('/')
const nextYearMonth = yearMonthList[index + 1];
const startDate = `${year}-${month}-01T00:00:00.000Z`;
const newStartDate = new Date(startDate);
// 月末日を取得
const endDate = new Date(new Date(newStartDate.setMonth(newStartDate.getMonth() + 1)).getTime() - 1).toISOString();
createPage({
path: `/archives/${year}/${month}/`,
component: archiveTemplate,
context: {
thisYear: year,
thisMonth: month,
prevPage: prevYearMonth,
nextPage: nextYearMonth,
periodStartDate: startDate,
periodEndDate: endDate
}
})
prevYearMonth = yearMonth;
})
ページ生成に使うテンプレートは年ごと月ごとでも同一のものを使いました。prevPage
とnextPage
に入るのがYYYY
かYYYY/MM
か、thisMonthがnullでないかで年別か年月別かで表示を切り替えます。
import React from "react";
import { graphql} from "gatsby";
import get from "lodash/get";
import Layout from "../components/layout";
import SEO from "../components/seo";
import ArticlePreview from "../components/article-preview";
import ArchivePagenation from "../components/archive-pagenation"
class ArchiveTemplate extends React.Component {
render() {
const posts = get(this, "props.data.allContentfulBlogPost.edges");
const totalCount = get(this, "props.data.allContentfulBlogPost.totalCount");
const {thisYear, thisMonth, prevPage, nextPage} = this.props.pageContext;
return (
<Layout location={this.props.location}>
<SEO
title={thisMonth ? `${thisYear}年${thisMonth}月の記事一覧` : `${thisYear}年の記事一覧`}
desc={thisMonth ? `${thisYear}年${thisMonth}月の記事一覧` : `${thisYear}年の記事一覧`}
noindex
/>
<div style={{ background: "#fff", textAlign: 'center' }}>
<div className="wrapper">
<h2 className="section-headline">{thisMonth ? `${thisYear}年${thisMonth}月の記事一覧` : `${thisYear}年の記事一覧`} ({totalCount}件)</h2>
<ul className="article-list">
{posts.map(({ node }) => {
return (
<li key={node.slug}>
<ArticlePreview article={node} />
</li>
);
})}
</ul>
<ArchivePagenation prevPage={prevPage} nextPage={nextPage} thisMonth={thisMonth} thisYear={thisYear} />
</div>
</div>
</Layout>
);
}
}
export default ArchiveTemplate;
export const archivePageQuery = graphql`
query BlogPostsByDate($periodStartDate: Date!, $periodEndDate: Date!) {
allContentfulBlogPost(filter: {publishDate: {gte: $periodStartDate, lte: $periodEndDate}}) {
edges {
node {
title
slug
publishDate(formatString: "YYYY年MM月DD日")
tags {
title
slug
}
heroImage {
fluid(maxWidth: 350, maxHeight: 196, resizingBehavior: THUMB) {
...GatsbyContentfulFluid_withWebp
}
}
description {
childMarkdownRemark {
excerpt
}
}
}
}
totalCount
}
}
`
アーカイブ用のページネーションコンポーネントに現在の年月情報と前後のページ情報を渡しています。上のディレクトリへのリンクもあること以外は基本的に記事のページネーションと同じなので、そのうち統合するかも。
import React from 'react'
import styled from '@emotion/styled'
import { Link } from 'gatsby'
const Wrapper = styled.div`
margin: 2em 0 0 0;
padding: 0 1.5em 2em;
`
const Box = styled.div`
display: flex;
justify-content: space-between;
margin: 0 auto;
width: 100%;
a {
padding: 1em;
border-radius: 2px;
text-decoration: none;
transition: 0.2s;
&:hover {
color: #fff;
background: #333;
}
}
`
const ArchivePagenation = ({ prevPage, nextPage, thisMonth, thisYear }) => {
return (
<Wrapper>
<Box>
{nextPage && (
<Link to={`/archives/${nextPage}/`} style={{marginRight: 'auto', order: 1}}>← {nextPage}</Link>
)}
{thisMonth ?
<Link to={`/archives/${thisYear}/`} style={{order: 2}}>{thisYear}年一覧へ</Link> :
<Link to="/archives/" style={{order: 2}}>アーカイブ一覧へ</Link>}
{prevPage && (
<Link to={`/archives/${prevPage}/`} style={{marginLeft: 'auto', order: 3}}>{prevPage} →</Link>
)}
</Box>
</Wrapper>
)
}
export default ArchivePagenation
こんな感じになりました。
年の一つ上の/archives/
では各年月のアーカイブページへのリンクがまとめられてると便利そうです。気づきやハマりポイントは以下の通りです。
見栄えは上から下に向けて時系列順で見えるようにしました。
import React from "react";
import { graphql, Link } from "gatsby";
import get from "lodash/get";
import styled from '@emotion/styled'
import Layout from "../components/layout";
import SEO from "../components/seo";
const Box = styled.div`
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
border: 1px solid #ccc;
margin: 1em;
vertical-align: middle;
align-items: center;
h3 {
display: block;
flex: 2;
margin: 0;
padding: 0;
a {
text-decoration: none;
}
}
ul {
margin: 0;
padding: 0;
flex: 2;
li {
display: block;
background-color: #eee;
border-radius: 10px;
margin: .25em;
a {
display: block;
padding: .5em;
border-radius: 10px;
width: 100%;
height: 100%;
&:hover {
background-color: #ccc;
}
}
}
}
`
class ArchivesIndex extends React.Component {
render() {
const posts = get(this, "props.data.allContentfulBlogPost.edges");
const totalCounts = {};
const years = new Set();
const yearMonths = new Set();
posts.forEach(post => {
const { year, yearMonth } = post.node;
// 年別、月別のtotalCountを追加
if (!totalCounts.hasOwnProperty(year)) totalCounts[year] = 0;
if (!totalCounts.hasOwnProperty(yearMonth)) totalCounts[yearMonth] = 0;
totalCounts[year] += 1;
totalCounts[yearMonth] += 1;
// years, yearMonths Set作成
years.add(year);
yearMonths.add(yearMonth);
})
const yearList = Array.from(years);
const yearMonthList = Array.from(yearMonths);
return (
<Layout location={this.props.location}>
<SEO title="Archives" desc="年月別アーカイブページ" noindex />
<div style={{ background: "#fff", textAlign: "center" }}>
<div className="wrapper">
<h2 className="section-headline">Archives</h2>
{yearList.map(year => {
return (
<Box key={`box${year}`}>
<h3 key={`header${year}`}><Link to={`/archives/${year}/`}>{year}年 ({totalCounts[year]})</Link></h3>
<ul key={`list${year}`}>
{yearMonthList.map(yearMonth => {
if (year === yearMonth.split('/')[0]) {
return (
<li key={yearMonth}>
<Link to={`/archives/${yearMonth}/`}>{yearMonth} ({totalCounts[yearMonth]})</Link>
</li>
)
}
})}
</ul>
</Box>
)
})}
<Link to={`../`}>HOME</Link>
</div>
</div>
</Layout>
);
}
}
export default ArchivesIndex;
export const pageQuery = graphql`
query ArchiveIndexQuery {
allContentfulBlogPost(sort: {fields: publishDate, order: DESC}) {
edges {
node {
year: publishDate(formatString: "YYYY")
yearMonth: publishDate(formatString: "YYYY/MM")
}
}
}
}
`;
結果
とりあえず年月別アーカイブが実装できました。aboutページ、関連記事、著者ページ、emotionへの完全に置き換え、Material UI導入など色々やりたいことも頭にありますが、適当にやっていこうと思います。