アイキャッチの画像

【Next.js 15】 サイト内検索(記事検索)機能をブログに実装する方法

公開日:

更新日:

デモ 完成イメージ

本サイトで使用しているサイト内検索機能(記事検索機能)を紹介します。↓

ポイント

・URLを読み取り、記事一覧ページでない場合は非表示になります。

・検索をかけた結果をsearch-resultページで表示している形になります。

・ライブラリなどは特に使用していません。

・検索は至ってシンプルで 入力されたキーワードを記事のdescription(要約)が含んでいるかで判断しています。

ディレクトリ構成&開発環境

フォルダ構成は下記の形になります。↓

フォルダ構成図

1/
2└ src
3  ├ app
4  │ ├ components
5  │ │   ├ article.json
6  │ │   ├ SearchArticle.tsx
7  │ │   └ SearchResult.tsx
8  │ └ search-result
9  │     └ page.tsx
10  └ layout.tsx
11

・記事データはJson形式で取得します。(データベースは使用しません)

SearchArticle.tsxで検索キーワードが入力されたらsearch-resultページに飛ばして結果を表示させます。

開発環境

・言語: Typescript


・ライブラリ: React


・フレームワーク: Next.js (バージョン:15.0.3 App Router形式)


・CSSフレームワーク: Tailwind CSS

コード実例

article.json

article.json

1
2{
3    "articleList": [
4        {
5            "genre":"記事のジャンル名",
6            "id":2,
7            "url": "記事のURL",
8            "date": "2025/05/14",
9            "description": "記事の説明文",
10            "image":"サムネイル画像のURL"
11        },
12        {
13            "genre":"記事のジャンル名",
14            "id":1,
15            "url": "記事のURL",
16            "date": "2025/05/13",
17            "description": "記事の説明文",
18            "image":"サムネイル画像のURL"
19        },
20    ]
21}        
22
23

article.jsonでは記事の情報を持ちます。

description内に書いてある文字列が検索キーワードと一致すれば検索ヒットとみなします。

SearchArticle.tsx

SearchArticle.tsx

1/* eslint-disable */
2"use client";
3import { Link, TextField } from "@mui/material";
4import { usePathname } from "next/navigation";
5import SearchIcon from "@mui/icons-material/Search";
6import { useEffect, useState } from "react";
7import { useRouter } from "next/navigation";
8import { useSearchParams } from "next/navigation";
9
10const SearchArticle = () => {
11  const router = useRouter();
12  const url = "search-result?keyword=";
13  const clear = () => {
14    setInputText("");
15  };
16  const handleClick = () => {
17    clear();
18  };
19  const [inputText, setInputText] = useState("");
20  const [query, setQuery] = useState(url);
21  const path = usePathname();
22  const searchParams = useSearchParams();
23
24  const checkPath =
25    path == "/" ||
26    path.includes("search-result");
27
28  useEffect(() => {
29    clear();
30  }, [path]);
31
32  return (
33    <>
34      <div
35        className={
36          !checkPath
37            ? "hidden"
38            : "visible   mt-3 w-full p-3  bg-white shadow-md rounded text-center "
39        }
40      >
41        <TextField
42          onKeyDown={(e) => {
43            if (e.keyCode === 13) {
44              // エンターキー押下時の処理
45              handleClick;
46              router.push(query);
47            }
48          }}
49          sx={{ width: "90%" }}
50          value={inputText}
51          onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
52            setInputText(event.target.value);
53            setQuery(url + event.target.value);
54          }}
55          margin="dense"
56          variant="outlined"
57          label="サイト内検索"
58          InputProps={{
59            endAdornment: (
60              <Link href={query}>
61                <button
62                  onClick={handleClick}
63                  className="my-2 -mr-1.5 bg-[#798777] border-gray-400 font-bold group relative inline-flex h-9 items-center justify-center overflow-hidden rounded-md border  w-20   text-white transition-all duration-100 [box-shadow:5px_5px_rgb(82_82_82)] hover:translate-x-[3px] hover:translate-y-[3px] hover:[box-shadow:0px_0px_rgb(82_82_82)] gap-2"
64                >
65                  <SearchIcon sx={{ fontSize: 20 }} />
66                  検索
67                </button>
68              </Link>
69            ),
70          }}
71        />
72      </div>
73    </>
74  );
75};
76
77export default SearchArticle;
78

checkPath変数で今どこのページなのかを判断します。記事一覧を表示するページだった場合はtrueとなりSearchArticleコンポーネントがvisibleになります。

TextFieldで何かキーワードが入った状態で検索ボタン(もしくはエンターキー)が押されるとsearch-resultページに飛びます。 その際キーワードをクエリパラメータとして送ります 。

page.tsx(search-resultページ)

page.tsx

1import { Suspense } from "react";
2import SearchResult from "../components/SearchResult";
3
4export default function Page() {
5  return (
6    <>
7      <Suspense>
8        <SearchResult />
9      </Suspense>
10    </>
11  );
12}
13

・後述するSearchResult.tsxではuseSearchParams()というメソッドを使用することになります。useSearchParams()を使用しているコンポーネントは<Suspense>でラップされなければなりません

SearchResult.tsx

SearchResult.tsx

1"use client";
2
3import Pagination from "../components/Pagination";
4import Breadcrumb from "../components/Breadcrumb";
5import { useSearchParams } from "next/navigation";
6import data from "../components/article.json";
7
8export default function SearchResult() {
9  const searchParams = useSearchParams();
10  const keyword = searchParams.get("keyword") ?? "";
11
12  let sortedData = data.articleList.filter((item: { description: string }) =>
13    item.description.includes(keyword)
14  );
15 
16
17  const showSearchParam = () => {
18      return (
19        <>
20          <div className="flex flex-row bg-gray-200 gap-1 mt-3   ">
21            <div className="bg-[#798777] w-1 "></div>
22            <h2 className="text-lg font-semibold p-1 ">
23              「 {keyword} 」の検索結果は
24              {sortedData.length}件です
25            </h2>
26          </div>
27        </>
28      );
29  };
30
31  const showResult = (dataSize: number) => {
32    if (dataSize == 0) {
33      return (
34        <>
35          <div className="flex flex-row justify-between">
36            <p className="text-lg mt-4">
37              他のキーワードでお探し頂くか、もしくはカテゴリー選択をご利用下さい。
38            </p>
39          </div>
40        </>
41      );
42    } else if (dataSize >= 1) {
43      return (
44        <>
45          <div className="flex flex-row justify-between">
46            <Pagination articles={sortedData} />
47          </div>
48        </>
49      );
50    }
51  };
52
53  return (
54    <>
55      <div className="mb-3  w-full">{showSearchParam()}</div>
56      <div className="w-full">
57        <div>{showResult(sortedData.length)}</div>
58      </div>
59    </>
60  );
61}
62

useSearchParams()はurlからクエリパラメータ(今回の場合だと?keyword=以降の部分)を取得するのに使用します。

Paginationコンポーネントは こちらの記事で解説していますので詳細を知りたい方はご覧下さい。

sortedDataが0件なら案内文言を表示します。1件以上ヒットした記事があれば記事一覧を表示します。

layout.tsx

layout.tsx

1import Footer from "./Footer";
2import "./globals.css";
3import Header from "./Header";
4import SearchArticle from "./components/SearchArticle";
5import { Suspense } from "react";
6
7export default function RootLayout({
8  children,
9}: Readonly<{
10  children: React.ReactNode;
11}>) {
12
13  return (
14    <html lang="js">
15      <body>
16        <div className="flex flex-col min-h-screen ">
17          <Header />
18
19          <main className="flex-grow  bg-slate-100 ">
20            <div className="md:flex h-full">
21              <section className="js-toc-content w-full md:w-2/3 flex flex-col items-center px-2 mt-4 pb-5">
22                {children}
23              </section>
24              <div className="md:w-1/3 flex flex-col items-center px-2 md:pl-1">
25                <Suspense>
26                  <SearchArticle />
27                </Suspense>
28              </div>
29            </div>
30          </main>
31
32          <Footer />
33        </div>
34      </body>
35    </html>
36  );
37}
38

SearchArticleコンポーネントでもuseSearchParams()メソッドを使用しているので<Suspense>でラップされなければなりません

layout.tsxSearchArticleを配置することで検索欄をサイトに表示しています。

まとめ

上記で紹介しているコンポーネントで検索機能を実現できます。

Profile Image
コピペでもそのまま使用できると思うので参考にしてみて下さい。

本記事は以上になります。

ご一読頂きありがとうございました。