【雑メモ】Next.jsのブログにAlgoliaの検索機能を導入した。

Next.js x Vercel で構成されている当ブログに Algolia 検索を導入しました。
検索

PR は以下にまとめています。
Feat/Algolia by kenzo-tanaka · Pull Request #205 · kenzo-tanaka/nextJsBlog

実装手順などを書きます。

必要なパッケージをインストール

npm install algoliasearch
npm install react-instantsearch-dom
npm install instantsearch.css # 検索をいい感じにスタイリングしてくれるCSSファイル

Algolia に登録/インデックスを追加/検索対象の属性登録

登録

Sign in | Algolia

インデックスを追加

インデックスの登録。JSON ファイルのアップロードができるので、ブログのマークダウンファイルのメタデータを JSON ファイルに書き出してアップロードする。

functions/test.ts
import fs from "fs";
import { getSortedPostsData } from "../lib/posts";

const getArticleMeta = () => {
  const posts = getSortedPostsData(); // メタデータを取得してくる
  const data = JSON.stringify(posts); // ファイルに書き込みできるよう変換

  fs.writeFile("algolia.json", data, (err) => {
    if (err) throw err;
    console.log("正常に書き込みが完了しました");
  });
};

getArticleMeta();

これを npm スクリプトで実行できるよう下準備する。

tsconfig.builder.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "module": "commonjs",
    "outDir": "dist",
    "noEmit": false
  },
  "exclude": ["node_modules"],
  "include": ["functions/*.ts"]
}
package.json
"scripts": {
  "algolia": "ts-node --project tsconfig.builder.json ./functions/test.ts",
},

この辺りの書き方はcatnose99/team-blog-hub: RSS based blog starter kit for teamsを参考にさせて頂きました。

npm run algolia

生成された JSON ファイルをダッシュボードの Upload file でアップロードする。

検索対象の属性を登録

僕は記事の slugtitleを検索対象としました。

検索機能の実装

スタイルを読み込み

pages/_app.tsx
import "instantsearch.css/themes/satellite.css";

環境変数のセット

ALGOLIA_APP_ID=xxx
ALGOLIA_API_KEY=xxx
next.config.js
module.exports = withPlugins([[optimizedImages, {}]], {
  // 省略
  env: {
    ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID,
    ALGOLIA_API_KEY: process.env.ALGOLIA_API_KEY,
  },
});

検索用コンポーネントを作成

components/search.tsx
import React from "react";
import algoliasearch from "algoliasearch/lite";
import Link from "next/link";
import {
  SearchBox,
  Hits,
  Highlight,
  InstantSearch,
} from "react-instantsearch-dom";

const algoliaSettings = {
  searchClient: algoliasearch(
    `${process.env.ALGOLIA_APP_ID}`,
    `${process.env.ALGOLIA_API_KEY}`
  ),
  // ここは自分が作ったインデックス名にする
  indexName: "kenzo_blog",
};

const Hit = ({ hit }: any) => {
  return (
    <div className="hit">
      <Link href={`/posts/${hit.slug}`}>
        <a>{hit.title}</a>
      </Link>

      <div className="hitName">
        <Highlight attribute="name" hit={hit} />
      </div>
    </div>
  );
};

const SearchResult = () => {
  return (
    <div className="search-result">
      <Hits hitComponent={Hit} />
    </div>
  );
};

const Search: React.FC = () => {
  return (
    <>
      <InstantSearch
        searchClient={algoliaSettings.searchClient}
        indexName={algoliaSettings.indexName}
      >
        <SearchBox translations={{ placeholder: "search..." }} />
        <SearchResult />
      </InstantSearch>
    </>
  );
};

export default Search;

検索用ページで読み込み

pages/search.tsx
import Layout from "../components/layout";
import Search from "../components/search";
import { PageSEO } from "../components/pageSEO";

const SearchPage = () => {
  return (
    <Layout>
      <Search />
      <PageSEO title="検索" />
    </Layout>
  );
};

export default SearchPage;