Stackbitブログに検索機能を追加する

Stackbitブログに検索機能を追加する

検索機能を追加しました!

こんにちは、junwatanabe72 です。
ブログサイトには検索機能はつきものですよね。
Stackbit で作成したブログには、記事検索機能がついていません。 なので、自分で実装してみました。

要件

  • 「 / > blog」ページに「キーワード検索」とセレクトボックスを追加する。
  • 記事の subtitle 要素をタグとして、使用する。
  • セレクトボックスのオプション要素には、記事につけられているタグの全てが含まれている。
  • セレクトボックスのタグを選択すると、タグがつけられている記事のみ表示される。

実装

便利ツール「material-ui」をインストールします。

yarn add @material-ui/core @material-ui/icons @material-ui/styles

今回編集するファイルは、「src/layouts/blog.js」です。
このファイルを編集していくのですが、 layout コンポーネントに要素を詰め込むことは 控え、新たにコンポーネント「BlogContent.js」を作成します。
ここでは、BlogContent を呼び込んで、blog 一覧を props で渡すことにとどめます。

src / layouts / blog.js;

import React from 'react';
import _ from 'lodash';
import moment from 'moment-strftime';
// 追加要素
import { Layout, BlogContent } from '../components/index';
//
import { Link, getPageUrl, withPrefix } from '../utils';

export default class Blog 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 posts = _.orderBy(_.get(this.props, 'posts', []), 'date', 'desc');

        return (
            <Layout page={page} config={config}>
                <header className="screen-reader-text">
                    <h1>{title}</h1>
                </header>
                <div className="post-feed">
                    // 追加要素
                    <BlogContent allArticles={posts} />
                    //
                </div>
            </Layout>
        );
    }
}

「src/components/」 BlogContent を作成します。

src / components / 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;
sass/imports/_posts-page.scss .search {
    display: flex;
    justify-content: flex-start;
    align-items: center;
    padding: 1vw;
}
.select-field {
    padding: 2vw !important;
    width: 30vw !important;
}

要点のみ解説します。

  • useState を使用して、ページ内の記事とタグを管理する。
  • タグは全ての記事に付けられているタグを取得し、重複を取り除く。
  • セレクトボックスは、material-ui を使用。一部 UI を scss で調整する
  • 検索マークも設置。material-ui から SearchIcon をインポート。

完成品

まとめ

そこまで難しい実装ではありませんでしたが、そもそも記事のタグ管理が subtitle で良いのか問題があります。 全体含めてブラッシュアップしていく必要がありそうです。

全然別件ですが、Stackbit 内の preview が表示されないエラーで半日費やしてしましました。 原因は、scss の justify-content が start だったことによる build エラーでした、flex-start にすることで解決。。もっと複雑なエラーだと思って試行錯誤しましたが、単純なミス。また一つ勉強になりました。

関連記事

PROGRAMの関連記事