佐藤 あゆみ
佐藤 あゆみ

necco Note

Pagefindでお手軽!静的HTML検索導入(Astro編)

  • Web Development

静的サイトに、検索機能を導入したいと考えたことはありませんか?

ウェブサイトに適切なナビゲーションがあればコンテンツには辿り着けるはずですが、コンテンツ量が多い場合や、ワケあって特定のページに最短で到達したい場合、そうとも言い切れないことがあります。そんな時に役立つのが「サイト内検索」機能です。

検索は動的な機能のため、HTMLで構成される静的サイトに導入するにあたっては、通例では外部のデータベースと連携するなどの工夫が必要になります。

そんな検索機能を静的サイトに搭載するために作られているのが、JavaScriptとJSONの静的アセットで動作する「Pagefind」です。Astroで生成したサイトはもちろん、静的なHTMLサイトであればどのようなサイトにも検索機能をつけられるツールです。

Pagefindとは

Pagefindは、静的サイトに特化した高速な全文検索ライブラリです。多言語対応やフィルタリング、カスタマイズ性の高いUIなど、豊富な機能を備えています。静的サイトジェネレーターを利用したサービスを提供しているCloudCannon社によって開発されています。

Pagefindの公式ドキュメントの検索機能にも、もちろんPagefindが使われており、動作を試せます。

Pagefindのウェブページのスクリーンショット。検索結果が表示されており、検索語句がハイライトされている。
Pagefindの公式ドキュメント。検索結果が表示されており、検索語句がハイライトされている。

Pagefindの導入

今回はAstro編ということで、Pagefindの最も手軽な導入方法として、すでに、Astroで構築済みのプロジェクトにPagefindとUIライブラリのPagefind UIをインストールしてみます。

ライブラリのインストール

まずは、プロジェクトに、検索のためのデータ収集(インデックス)をするためのライブラリであるPagefindと、デフォルトのUIライブラリのPagefind UIをインストールします。


$ npm install pagefind @pagefind/default-ui

ビルドコマンドの設定を変更する

Pagefindは、ビルド後のHTMLファイルからデータを収集し、検索用のインデックスを生成します。
Astroのビルド時にPagefindがインデックスを生成できるよう、package.jsonのビルドコマンドにPagefindのインデックス用コマンドを追記します。
Astroでは、デフォルトでは/dist配下にファイルが生成されますので、オプションでdistディレクトリを指定します。

package.json

"scripts": {
	"dev": "astro dev",
	"start": "astro dev",
	"build": "astro build && pagefind --site dist", // 追記
	"preview": "astro preview",
	"astro": "astro",
},

ビルドする

コマンドを打ってビルドすると、HTMLとインデックスが生成されます。ビルドログからは、57ページから3991ワードをインデックスできたことを読み取れます。

$ npm run build
Pagefind v1.1.0のコマンドライン出力。"dist"をソースとして、"dist/pagefind"ディレクトリに出力しています。58個のHTMLファイルを見つけ、サイト上のすべての<body>要素をインデックス化しています。

検索UIを設置する

検索用のデータを用意できましたので、次は、Pagefind UIを利用して、検索の入力欄をサイト内に設置します。

src/components/common/search/SearchForm.astroを作成します。
※コンポーネントを配置するディレクトリやファイル名は任意で指定してOKです。

SearchForm.astro


---
import '@pagefind/default-ui/css/ui.css'
---

<div id="search"></div>

<script>
  import { PagefindUI } from '@pagefind/default-ui'
  new PagefindUI({
  element: '#search',
})
</script>

このコンポーネントを、サイト内の検索UIを表示したい箇所から呼び出します。

---
import SearchForm from "@/components/Search/SearchForm.astro";
---

(中略)

<SearchForm />

npm run devコマンドで開発プレビューを立ち上げると、フォームが設置できています。
ただしこの時点では、キーワードを入力しても検索できません。
検索の動作を試すには、ビルド結果を実行する npm run preview でプレビューする必要があります。

検索バーに「test」と入力されているインターフェースの画像。検索バーの左側には虫眼鏡のアイコンがあり、検索バーの下には「testを検索しています」というテキストが表示されています。

previewコマンドを利用すると、今度は検索結果が表示されました。
サムネイル画像が表示され、検索キーワードもハイライトされており、とても見やすい検索結果になっています。

検索結果のスクリーンショット

たったこれだけの手順で、とても手軽にウェブサイトにページ内検索を導入できました。
色々と試していくと、日本語での検索結果は完璧とは言えませんが、キーワード単位での検索など、通常利用するには支障がないレベルです。

痒いところに手が届く設定も

検索対象からの除外、あるいは検索へのメタデータの追加などにももちろん対応しています。

インデックスから除外する

Pagefindは、デフォルトではHTMLの<body>内の全ての文言をインデックスします。

特定のエリアだけを検索範囲としたいときは、下記のようにdata-pagefind-body属性を付与します。

<main data-pagefind-body>
  <h1>サンプル</h1>
  <p>サンプルテキストです。</p>
</main>

逆に、特定の範囲のみを検索対象から除外したい場合はdata-pagefind-ignore属性を利用します。

<main data-pagefind-body>
  <h1>サンプル</h1>
  <p data-pagefind-ignore>このテキストは検索対象から除外されます。</p>
</main>

これらを利用すれば、「記事ページ内の関連記事のエリア」など、検索結果には含めたくない内容を除外できるため、検索結果をクリーンな状態に保てます。

検索結果に日付を表示する

HTMLの属性にて、data-pagefind-metaとして任意の値を渡すと検索結果に表示できます。
下記の例では、日付を検索結果に表示しています。

<!-- 要素からメタデータを抜き出す -->
<time data-pagefind-meta="date">2024-06-01</time>

<!-- 属性からメタデータを抜き出す -->
<time dateTime="2024-06-01" data-pagefind-meta="date[dateTime]">
  2024年6月1日
</time>

<!-- インラインでメタデータを指定する -->
<h1 data-pagefind-meta="date:2024-06-01">Pagefindでお手軽検索</h1>

検索結果をソートする

data-pagefind-sort属性を付与し、オプション設定すれば、検索結果を日付順など任意の値でソートできます。設定の変更後は、ビルドとプレビューの再起動が必要です。 ソート内容の変更用のUIは用意されておらず、また、ソートが設定されていないページは検索結果から無視されるようです。

<time data-pagefind-meta="date" data-pagefind-sort="date">2024-06-01</time>
<script>
  import { PagefindUI } from '@pagefind/default-ui'

  new PagefindUI({
    element: '#search',
    sort: { date: 'asc' },
  })
</script>

その他のオプションとして、データの重みづけ、フィルタ、多言語サポートなどがあり、ドキュメントから利用方法を確認できます。

検索UIをCSSでカスタマイズする

SearchForm.astroで@pagefind/default-ui/css/ui.cssをインポートしましたが、このデフォルトのCSSを読み込まずに、独自でCSSを書けば、オリジナルの見た目で検索UIや結果を作成できます。CSSですので、Astro内でCSS変数やクラス名を上書きすることでも見た目を変更できます。

独自UIで検索ページを作るには

完全にオリジナルの機能と見た目で検索UIや検索結果を作成したい場合は、JavaScript APIを利用します。

Using the Pagefind search API(公式ドキュメント)

私も少々試してみましたが、やはり、何から何までを自前で実装しようと思うと「気軽に」とはいかないため、可能であれば既存のPagefind UIのCSSをアレンジするにとどめた方が良いのではという感想を持ちました。

試してみたい方のためにコードを置いておきますが、正直なところあまり良い実装には思えないので、Astroを活かした賢い実装ができる方にサンプルコードを見せていただきたいと思っています…!
下記のコードではTailwindを利用しています。

OriginalSearchForm.astro

---
---

<div class="mt-8 flex justify-center">
  <div class="relative w-full max-w-md">
    <input
      type="text"
      id="search-input"
      class="w-full rounded-md border border-gray-300 bg-white px-4 py-2 pr-12 text-gray-700 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
      placeholder="検索ワードを入力"
    />
  </div>
</div>

<div
  id="search-results"
  class="mt-8 grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3"
>
</div>

<template id="search-result-template">
  <div class="search-result overflow-hidden rounded-md bg-white shadow-md">
    <div
      class="h-40 w-full bg-cover bg-center"
      style="background-image: url('%%image%%')"
    >
    </div>
    <div class="p-4">
      <h3 class="text-lg font-bold">%%title%%</h3>
      <p class="mt-2 text-gray-700">%%excerpt%%</p>
      <a href="%%url%%" class="mt-4 block text-blue-500 hover:text-blue-600"
        >詳細を見る</a
      >
    </div>
  </div>
</template>

<div id="search"></div>
<script is:inline>
  document
    .getElementById('search-input')
    .addEventListener('input', async (e) => {
      const searchInput = document.getElementById('search-input')
      const searchResults = document.getElementById('search-results')

      const pagefind = await import('/pagefind/pagefind.js')
      await pagefind.init()
      const search = await pagefind.search(searchInput.value)

      if (search && search.results) {
        searchResults.innerHTML = ''
        const template = document.getElementById(
          'search-result-template'
        ).innerHTML

        for (const result of search.results.slice(0, 9)) {
          const resultData = await result.data()
          let resultHTML = template
            .replace('%%image%%', resultData.meta.image || '/placeholder.png')
            .replace('%%title%%', resultData.meta.title)
            .replace('%%excerpt%%', resultData.excerpt)
            .replace('%%url%%', resultData.url)

          const resultElement = document.createElement('div')
          resultElement.innerHTML = resultHTML
          searchResults.appendChild(resultElement.firstElementChild)
        }
      } else {
        searchResults.innerHTML =
          '<p class="text-gray-700 text-center">検索結果がありません</p>'
      }
    })
</script>

上記のコードを利用すると、カード型のUIで検索結果を表示できます。

終わりに

Pagefindを使うと、とても手軽に既存のAstroプロジェクトに検索機能を追加できました。

冒頭で書いた通り、Astro以外の静的HTMLジェネレータはもちろん、ジェネレーターを使わないサイトにも検索機能をつけられます。

私が過去にDreamweaverで制作したサイトにも検索機能をつけられました。
この場合は、サイトのルートディレクトリで下記のコマンドを実行してインデックスを生成します。

$ npx pagefind --site ./

そして、検索UIを表示したいページで下記を読み込むだけでした。お手軽にも程がありますね。

<link href="/pagefind/pagefind-ui.css" rel="stylesheet">
<script src="/pagefind/pagefind-ui.js"></script>
<div id="search"></div>
<script>
  window.addEventListener('DOMContentLoaded', (event) => {
    new PagefindUI({ element: "#search" });
  });
</script>

何はともあれ、「あればOK」の用途であれば数秒で設置できてしまう、とても優秀なPagefindライブラリ。
ぜひ一度お試しください!

佐藤 あゆみ

佐藤 あゆみ

Ayumi Sato

ニューヨーク生まれ。まもなく東京に移住し、1994年から2年間のオーストラリアでの生活を経て、ふたたび東京へ戻り、今も暮らす。1997年頃より趣味としてWeb制作を始め、以後も独学で学ぶ。 音楽専門学校中退後、音楽活動での成功を夢見ながら、PCパーツショップやバイク輸出入会社、楽器店など、掛け持ち含めて計20以上(?)の業種でアルバイトを重ね、ECサイトの運営管理や自社サーバの管理、プログラミングなども学ぶ。音楽活動を展開する中で、集客やフライヤー制作、プロモーションビデオ制作を行い、周辺技術を身につけるきっかけとなるも、2011年頃に区切りをつけ、ウェブ制作で生計を立てることを決意。その後は画廊やウェブ制作会社などで勤め、2014〜2022年まではフリーランスとして活動。2018年より、CSS NiteやBAU-YAなどのイベント、スクールにて登壇。2019年に「HTMLコーダー&ウェブ担当者のためのWebページ高速化超入門」を出版。 趣味はガジェットいじり&新しいサービスを試すこと。

SHARE

Other Note

necco Note

一つの制作会社にデザインと実装を一貫して依頼するメリットとは?