microCMS

AstroとmicroCMSを使った検索機能を実装する

千葉大輔

こんにちは、でぃーすけと申します。
今回はWebフレームワークAstroを使ってmicroCMSにおける検索機能を実装する方法をご紹介します。

はじめに

事前準備として以下の公式ブログを参考に「ブログサイト」を構築してください。
https://blog.microcms.io/astro-microcms-introduction/

今回は上記で作成したブログサイトをベースに検索機能を実装していきます。
また、UIフレームワークとしてPreact、データフェッチライブラリとしてSWRを使用しますので、以下の公式ブログを参考にそれぞれのライブラリを導入してください。
https://blog.microcms.io/astro-preview/#h00c1750207

実装の全体像

全体像としては先ほど挙げた「AstroとmicroCMSを使った画面プレビューを実装する」の記事でのやり方とかなり近いものになります。

  1. pagesディレクトリに検索ページを作成する
  2. クライアントJavaScriptで検索キーワードをURLクエリパラメータに追加する
  3. クライアントJavaScriptでURLクエリパラメータから検索キーワードを取得する
  4. クライアントJavaScriptでmicroCMSにリクエストを送り、取得したデータをレンダリングする

検索ページを作成する

まずは検索が行われるページを作成します。
今回は /search で開かれるページを作成したいので、一旦最小限の形で src/pages/search.astro を作成します。

---
import Layout from "../layouts/Layout.astro";
---

<Layout title="My first blog with Astro">
  <main></main>
</Layout>

検索キーワードをURLクエリパラメータに追加する

「キーワードを入力して確定すると /search?q=<検索キーワード> に遷移する」ようにPreactを使って実装していきます。

まずはUI部分を実装します。

const SearchButton = () => {
  return (
    <form
      role="search"
    >
      <label for="blog_search">
        記事を検索
      </label>
      <input
        id="blog_search"
        type="search"
      />
      <button>検索</button>
    </form>
  );
};

export default SearchButton;

参考:https://developer.mozilla.org/ja/docs/Web/Accessibility/ARIA/Roles/Search_role

次に、inputタグに入力された値をPreactで管理して制御されたコンポーネントとしてレンダリングするための実装を追加します。
(今回のような管理したステートを他に使用していない場合は、制御されていないコンポーネントとして扱っても複雑にならないかと思います)

import { useState } from "preact/hooks";
import type { JSXInternal } from "preact/src/jsx";

const SearchButton = () => {
  const [value, setValue] = useState(
    typeof window !== "undefined"
      ? new URLSearchParams(window.location.search).get("q") ?? ""
      : ""
  );
  const handleChange: JSXInternal.GenericEventHandler<HTMLInputElement> = (
    event
  ) => {
    setValue((event.target as HTMLInputElement).value);
  };

  return (
    <form
      role="search"
    >
      <label for="blog_search">
        記事を検索
      </label>
      <input
        id="blog_search"
        type="search"
        value={value}
        onChange={handleChange}
      />
      <button>検索</button>
    </form>
  );
};

export default SearchButton;

※本来Preactでは(ReactでいうonChangeは)onInputを使用するのですが、preact/compat を使用している場合は onChange で問題ありません。

これでキーワードの入力がステートとして管理されるようになったので、formのsubmitイベントで検索ページに遷移するようにします。

import { useState } from "preact/hooks";
import type { JSXInternal } from "preact/src/jsx";

const SearchButton = () => {
  const [value, setValue] = useState(
    typeof window !== "undefined"
      ? new URLSearchParams(window.location.search).get("q") ?? ""
      : ""
  );
  const handleChange: JSXInternal.GenericEventHandler<HTMLInputElement> = (
    event
  ) => {
    setValue((event.target as HTMLInputElement).value);
  };
  const handleSubmit: JSXInternal.GenericEventHandler<HTMLFormElement> = (
    event
  ) => {
    event.preventDefault();
    window.location.href = `/search?q=${value}`;
  };

  return (
    <form
      role="search"
      onSubmit={handleSubmit}
    >
      <label for="blog_search">
        記事を検索
      </label>
      <input
        id="blog_search"
        type="search"
        value={value}
        onChange={handleChange}
      />
      <button>検索</button>
    </form>
  );
};

export default SearchButton;

このコンポーネントをTOPページのAstroファイルに追記しましょう。
検索キーワードを入力後、エンターを押して、/search?q=<検索キーワード> に遷移できれば成功です。

URLクエリパラメータから検索キーワードを取得する

それでは検索ページの実装に移りましょう。
このページでは先ほどURLクエリパラメータに追加した検索キーワードをもとに記事を検索します。
そのための準備として、q パラメータを取得しましょう。

const BlogSearch = () => {
  const params = new URLSearchParams(window.location.search);
  const q = params.get("q");

  console.log(q);

  return (
    <div></div>
  );
};

export default BlogSearch;

このようなコンポーネントを作成し、src/pages/search.astro 内に追加します。

---
import BlogSearch from "../components/BlogSearch";
import SearchButton from "../components/SearchButton";
import Layout from "../layouts/Layout.astro";
---

<Layout title="My first blog with Astro">
  <main>
    <SearchButton client:visible />
    <BlogSearch client:only="preact" />
  </main>
</Layout>

その上で先ほどの入力UIから検索ページに移動し、コンソールの表示を確認してみましょう。
入力したキーワードが表示されていれば成功です。

microCMSにリクエストを送り、取得したデータをレンダリングする

次は実際にmicroCMSにリクエストを送ります。
先ほど取得した検索キーワードをGET APIのパラメータに指定することで全文検索ができます。

import useSWR from "swr";
import { getBlogs } from "../library/microcms";

const BlogSearch = () => {
  const params = new URLSearchParams(window.location.search);
  const q = params.get("q");

  const { data } = useSWR(
    q === null ? null : ["/search", q],
    ([, q]) =>
      getBlogs({
        fields: ["id", "title"],
        q,
      })
  );

  console.log(data);

  return (
    <div></div>
  );
};

export default BlogSearch;

※useSWRの使い方等はドキュメントサイトをご覧いただくか、前回のブログでも軽くご紹介させていただいておりますのでそちらをご確認ください。

コンソールを確認してみて、undefined から microCMSのデータに変われば成功です。
TOPページと同様に記事のリンクを表示しましょう。

import useSWR from "swr";
import { getBlogs } from "../library/microcms";

const BlogSearch = () => {
  const params = new URLSearchParams(window.location.search);
  const q = params.get("q");

  const { data, error, isLoading } = useSWR(
    q === null ? null : ["/search", q],
    ([, q]) =>
      getBlogs({
        fields: ["id", "title"],
        q,
      })
  );

  if (error) return <div>エラーが発生しました</div>;

  if (isLoading) return <div>読み込み中...</div>;

  return (
    <div>
      {data?.contents.length !== 0 ? (
        <ul>
          {data?.contents.map(({ id, title }) => (
            <li key={id}>
              <a href={id}>{title}</a>
            </li>
          ))}
        </ul>
      ) : (
        <div>検索結果はありません</div>
      )}
    </div>
  );
};

export default BlogSearch;

この実装でも10件まで検索結果は表示できますが、11件以上検索結果があった場合に表示ができないので簡単にページングを実装していきましょう。

import useSWR from "swr";
import { useState } from "preact/hooks";
import { getBlogs } from "../library/microcms";

const LIMIT = 10;

const BlogSearch = () => {
  const params = new URLSearchParams(window.location.search);
  const q = params.get("q");

  const [page, setPage] = useState(1);

  const { data, error, isLoading } = useSWR(
    q === null ? null : ["/search", q, page],
    ([, q, page]) =>
      getBlogs({
        fields: ["id", "title"],
        q,
        limit: LIMIT,
        offset: (page - 1) * LIMIT,
      })
  );

  if (error) return <div>エラーが発生しました</div>;

  if (isLoading) return <div>読み込み中...</div>;

  return (
    <div>
      {data?.contents.length !== 0 ? (
        <>
          <ul>
            {data?.contents.map(({ id, title }) => (
              <li key={id}>
                <a href={id}>{title}</a>
              </li>
            ))}
          </ul>
          {data?.totalCount !== undefined && (
            <nav>
              <ul style={{ display: "flex", gap: "8px" }}>
                {Array.from({
                  length: Math.ceil(data.totalCount / LIMIT),
                }).map((_, i) => (
                  <li key={i} style={{ listStyle: "none" }}>
                    <button
                      onClick={() => setPage(i + 1)}
                      disabled={page === i + 1}
                    >
                      {i + 1}
                    </button>
                  </li>
                ))}
              </ul>
            </nav>
          )}
        </>
      ) : (
        <div>検索結果はありません</div>
      )}
    </div>
  );
};

export default BlogSearch;

page というステートを持たせて、これをもとに表示されている検索結果を切り替えています。

そして、microCMSのパラメータとしては limitoffset を指定しています。(limit に関してはデフォルトと同じ10にしているので指定しなくても構いません。)
これによって page の値によって取得される記事が変化し、ページごとに適切な記事を取得できます。

ページを切り替えてみて記事リンクが適切に切り替われば成功です。

おわりに

今回はかなり最小限の形で検索機能を実装しました。
これをベースにして、以下のような実装をすることでさらにユーザビリティを上げることができると思います!

まずは、無料で試してみましょう。

APIベースの日本製ヘッドレスCMS「microCMS」を使えば、 ものの数分でAPIの作成ができます。

microCMSを無料で始める

microCMSについてお問い合わせ

初期費用無料・14日間の無料トライアル付き。ご不明な点はお気軽にお問い合わせください。

お問い合わせ

microCMS公式アカウント

microCMSは各公式アカウントで最新情報をお届けしています。
フォローよろしくお願いします。

  • twitter
  • facebook
  • github

ABOUT ME

千葉大輔
microCMSでCRE / FE をしています。Twitterでは「でぃーすけ」という名前で活動しており、ReactやNext.js、TypeScript、TailwindCSSが特に好きです。前職のWeb制作会社での経験を活かして、さまざまなユーザー体験を向上させたいです!