Stackbit ブログ(Next.js)の記事ページに関連記事を表示させました!
こんにちは、junwatanabe72 です。
ブログの記事ページには関連記事を表示させたいですよね。
Stackbit ブログのデフォルトにはない機能なので、今回も自分で実装してみました。
「関連記事表示」の要件
- 記事ページの最下段に関連記事を表示させる。
- 記事ページのカテゴリーと同カテゴリーの最新記事が関連記事として表示される。
- 関連記事は最新の記事から2つのみ表示される。
「関連記事表示」の実装
1.ライブラリのインストール
今回は特にありません。
2.ファイルの編集及び新規作成
編集するファイル:「src/components/BlogContent.js」、「src/layouts/post.js」
新規作成ファイル:「PostList.js」、「SearchBar.js」、「RelationContent.js」
2.1 BlogContent.js の編集
編集というより分解します。具体的には、検索バーと記事一覧を別のコンポーネントに分けます。 分離させた記事一覧のコンポーネントは関連記事を表示させる際に流用します。
これを
こうします
以下は、元々の BlogContent.js です。
BlogContent.js;
import React, { useState } from 'react';
import _ from 'lodash';
import SearchIcon from '@material-ui/icons/Search';
import TextField from '@material-ui/core/TextField';
import moment from 'moment-strftime';
import MenuItem from '@material-ui/core/MenuItem';
import { Link, getPageUrl, withPrefix } from '../utils';
const BlogContent = ({ allArticles }) => {
const [articles, setArticles] = useState(allArticles);
const [keyward, setKeyward] = useState('');
const renderPost = (post, index) => {
const title = _.get(post, 'title');
const thumbImage = _.get(post, 'thumb_img_path');
const thumbImageAlt = _.get(post, 'thumb_img_alt', '');
const excerpt = _.get(post, 'excerpt');
const date = _.get(post, 'date');
const dateTimeAttr = moment(date).strftime('%Y-%m-%d %H:%M');
const formattedDate = moment(date).strftime('%B %d, %Y');
const postUrl = getPageUrl(post, { withPrefix: true });
return (
<article key={index} className="post post-card">
<div className="post-inside">
{thumbImage && (
<Link className="post-thumbnail" href={postUrl}>
<img src={withPrefix(thumbImage)} alt={thumbImageAlt} />
</Link>
)}
<header className="post-header">
<h2 className="post-title">
<Link href={postUrl} rel="bookmark">
{title}
</Link>
</h2>
</header>
{excerpt && (
<div className="post-content">
<p>{excerpt}</p>
</div>
)}
<footer className="post-meta">
<time className="published" dateTime={dateTimeAttr}>
{formattedDate}
</time>
</footer>
</div>
</article>
);
};
const changeArticles = (keyward) => {
const tmp = allArticles.filter((v) => {
const tags = v.subtitle.split(' ');
return tags.includes(keyward);
});
setArticles(tmp);
return;
};
const handleChange = (event) => {
setKeyward(event.target.value);
changeArticles(event.target.value);
return;
};
const options = () => {
const list = [];
Object.values(allArticles).forEach((v) => {
const tmp = v.subtitle.split(' ');
tmp.map((v) => list.push(v));
});
return [...new Set(list)];
};
return (
<>
<div className="search">
<div>キーワード検索</div>
<SearchIcon />
<TextField className="select-field" id="select-keyward" select onChange={handleChange} value={keyward}>
{[...options()].map((option) => (
<MenuItem key={option} value={option}>
{option}
</MenuItem>
))}
</TextField>
</div>
<div className="post-feed-inside">
{articles.map((post, index) => {
return renderPost(post, index);
})}
</div>
</>
);
};
export default BlogContent;
修正後の BlogContent.js です。
BlogContent.js;
import React, { useState } from 'react';
import _ from 'lodash';
import SearchBar from './atoms/SearchBar';
import PostList from './PostList';
const BlogContent = ({ allArticles }) => {
const [articles, setArticles] = useState(allArticles);
const [keyword, setKeyword] = useState('');
const changeArticles = (keyword) => {
const tmp = allArticles.filter((v) => {
return v.subtitle.includes(keyword);
});
setArticles(tmp);
return;
};
const handleChange = (event) => {
setKeyword(event.target.value);
changeArticles(event.target.value);
return;
};
const options = () => {
const list = [];
Object.values(allArticles).forEach((v) => {
v.subtitle.map((v) => list.push(v));
});
return [...new Set(list)];
};
return (
<>
<SearchBar onChange={handleChange} keyword={keyword} options={options} />
<PostList articles={articles} />
</>
);
};
export default BlogContent;
SearchBar.js です。
SearchBar.js;
import React from 'react';
import _ from 'lodash';
import SearchIcon from '@material-ui/icons/Search';
import TextField from '@material-ui/core/TextField';
import MenuItem from '@material-ui/core/MenuItem';
const SearchBar = ({ onChange, keyward, options }) => {
return (
<div className="search">
<div>キーワード検索</div>
<SearchIcon />
<TextField className="select-field" id="select-keyward" select onChange={onChange} value={keyward}>
{[...options()].map((option) => (
<MenuItem key={option} value={option}>
{option}
</MenuItem>
))}
</TextField>
</div>
);
};
export default SearchBar;
PostList.js です。
PostList.js;
import React from 'react';
import _ from 'lodash';
import moment from 'moment-strftime';
import { Link, getPageUrl, withPrefix } from '../utils';
const PostList = ({ articles }) => {
const renderPost = (post, index) => {
const title = _.get(post, 'title');
const thumbImage = _.get(post, 'thumb_img_path');
const thumbImageAlt = _.get(post, 'thumb_img_alt', '');
const excerpt = _.get(post, 'excerpt');
const date = _.get(post, 'date');
const dateTimeAttr = moment(date).strftime('%Y-%m-%d %H:%M');
const formattedDate = moment(date).strftime('%B %d, %Y');
const postUrl = getPageUrl(post, { withPrefix: true });
return (
<article key={index} className="post post-card">
<div className="post-inside">
{thumbImage && (
<Link className="post-thumbnail" href={postUrl}>
<img src={withPrefix(thumbImage)} alt={thumbImageAlt} />
</Link>
)}
<header className="post-header">
<h2 className="post-title">
<Link href={postUrl} rel="bookmark">
{title}
</Link>
</h2>
</header>
{excerpt && (
<div className="post-content">
<p>{excerpt}</p>
</div>
)}
<footer className="post-meta">
<time className="published" dateTime={dateTimeAttr}>
{formattedDate}
</time>
</footer>
</div>
</article>
);
};
return (
<div className="post-feed-inside">
{articles.map((post, index) => {
return renderPost(post, index);
})}
</div>
);
};
export default PostList;
2.2 post.js の編集
関連記事を表示させる RelationContent コンポーネントを読み込みます。
なお、props として渡している relationArticles は自身の記事を除いた記事一覧です。 本来であれば、keyword ごとに関連記事を表示させるべきですが、次回の実装とします。
post.js;
import React from 'react';
import _ from 'lodash';
import moment from 'moment-strftime';
import { Layout } from '../components/index';
import RelationContent from '../components/atoms/RelationContent';
import ShareButtons from '../components/atoms/ShareButtons';
import { htmlToReact, withPrefix, markdownify } from '../utils';
export default class Post extends React.Component {
render() {
const data = _.get(this.props, 'data');
const config = _.get(data, 'config');
const page = _.get(this.props, 'page');
const title = _.get(page, 'title');
const subtitle = _.get(page, 'subtitle');
const image = _.get(page, 'content_img_path');
const imageAlt = _.get(page, 'content_img_alt', '');
const date = _.get(page, 'date');
const dateTimeAttr = moment(date).strftime('%Y-%m-%d %H:%M');
const formattedDate = moment(date).strftime('%A, %B %e, %Y');
const markdownContent = _.get(page, 'markdown_content');
const posts = _.orderBy(_.get(this.props, 'posts', []), 'date', 'desc');
const { __metadata } = page;
const URL = `https://junwatanabe72.com${__metadata.urlPath}`;
const relationArticles = posts.filter((post) => post.title !== title);
return (
<Layout page={page} config={config}>
<article className="post post-full">
<header className="post-header inner-sm">
<h1 className="post-title underline">{title}</h1>
</header>
<div className="inner-sm">
<ShareButtons url={URL} text={title} />
</div>
{image && (
<div className="post-image">
<img src={withPrefix(image)} alt={imageAlt} />
</div>
)}
{markdownContent && <div className="post-content inner-sm">{markdownify(markdownContent)}</div>}
<footer className="post-meta inner-sm">
<time className="published" dateTime={dateTimeAttr}>
{formattedDate}
</time>
</footer>
<RelationContent articles={relationArticles} keyword={subtitle[0]} />
</article>
</Layout>
);
}
}
2.3 RelationContent.js の新規作成
キーワードでフィルタリングした filteredArticles を PostList に props として渡しています。 PostList をコンポーネント化したため、使い回すことができるようになりました。
RelationContent.js;
import React from 'react';
import _ from 'lodash';
import FavoriteIcon from '@material-ui/icons/Favorite';
import PostList from '../PostList';
const title = '関連記事';
const RelationContent = ({ articles, keyword }) => {
const filteredArticles = articles.filter((article, num) => {
const keys = article.subtitle;
return keys.includes(keyword) && num < 2;
});
return (
<div className="inner-sm">
<div className="common-flex">
<FavoriteIcon fontSize="large" color="secondary" />
<h3 className="ignore">{title}</h3>
</div>
{filteredArticles.length !== 0 && (
<>
<h5>{`${keyword}の${title}`}</h5>
<PostList articles={filteredArticles} />
</>
)}
</div>
);
};
export default RelationContent;
3.実装の要点
- BlogContent.js を分解し、「SearchBar.js」,「PostList.js」を新規作成する。
- 関連記事を表示させる「RelationContent.js」を作成し、「PostList.js」を流用する。
- 「post.js」内で「RelationContent.js」を読み込む。
- keyword ごとに関連記事を分ける実装は次回以降。
「関連記事表示」の完成品
まとめ
コンポーネントの粒度を細かくして、いかに使い回すかが、react の面白いところですよね。 コードを書くだけじゃなくて、設計をしっかり考えないといけませんね!