當網站內容一多的話,很難快速找到我們想要的文章。
搜尋功能可以幫助用戶輕鬆找到所需的內容,提升使用者體驗,並提高網站的互動性和價值。
今天,將介紹如何在 Astro 框架中實現一個基本的搜尋系統,以加強你的網站。
以下是完成後的頁面,分別是搜尋後顯示的列表與未搜尋到的畫面
首先,我們需要引入 DOMPurify 套件,這個套件用於加強網站和應用程序的安全性,特別是在接受用戶提供的 HTML 內容時。它能夠檢測和清理潛在的跨站腳本攻擊(XSS)向量,同時確保 HTML 代碼符合標準和最佳實踐。
npm i dompurify
接下來,在src/components/ 建立 SearchWidget.astro 搜尋元件
---
---
<form class="form" action="">
  <div class="flex h-12 items-center px-3 border border-gray-300 rounded-lg">
    <input
      class="w-full placeholder:text-primary-200 focus:outline-none focus:ring-transparent"
      placeholder="Search posts"
      name="search"
      id="search"
      min="2"
      max="24"
    />
    <button>
      <svg
        class="text-gray-600 h-4 w-4 fill-current"
        xmlns="http://www.w3.org/2000/svg"
        xmlns:xlink="http://www.w3.org/1999/xlink"
        version="1.1"
        id="Capa_1"
        x="0px"
        y="0px"
        viewBox="0 0 56.966 56.966"
        style="enable-background:new 0 0 56.966 56.966;"
        xml:space="preserve"
        width="512px"
        height="512px"
      >
        <path
          d="M55.146,51.887L41.588,37.786c3.486-4.144,5.396-9.358,5.396-14.786c0-12.682-10.318-23-23-23s-23,10.318-23,23  s10.318,23,23,23c4.761,0,9.298-1.436,13.177-4.162l13.661,14.208c0.571,0.593,1.339,0.92,2.162,0.92  c0.779,0,1.518-0.297,2.079-0.837C56.255,54.982,56.293,53.08,55.146,51.887z M23.984,6c9.374,0,17,7.626,17,17s-7.626,17-17,17  s-17-7.626-17-17S14.61,6,23.984,6z"
        ></path>
      </svg>
    </button>
  </div>
</form>
<script>
  import DOMPurify from 'dompurify';
  const form: HTMLFormElement = document.querySelector('form')!;
  form?.addEventListener('submit', (e) => {
    e.preventDefault();
    const formData = new FormData(form);
    const searchTerm = DOMPurify.sanitize(formData.get('search')?.toString());
    if (!searchTerm || searchTerm.length === 0) return;
    const url = new URL('/search', window.location.origin);
    url.searchParams.set('q', searchTerm);
    window.location.assign(url.toString());
  });
</script>
html
在這個元件中,我們建立了一個包含搜索輸入框和搜索按鈕的表單。
js
處理搜索請求,並將搜索結果顯示在搜尋結果頁面中。
在這段程式碼裡添加了一個事件監聽器,以捕捉搜索表單的提交事件。
這個元件的關鍵功能是當用戶提交搜尋時,它將用戶輸入的搜尋詞進行淨化(DOMPurify),然後將其附加到 URL 上,將用戶導向到搜尋結果頁面。這有助於實現即時搜尋功能。
在 src/pages 建立一支 search.json.ts
import { getCollection } from "astro:content";
import { sortByDate } from "../lib/sortByDate";
async function getPosts() {
  const posts = await getCollection("blog");
  const sortedPosts = sortByDate(posts);
  return sortedPosts.map((post) => {
    return {
      slug: post.slug,
      title: post.data.title,
      date: post.data.pubDate,
    };
  });
}
export async function get({}) {
  return new Response(JSON.stringify(await getPosts()), {
    status: 200,
    headers: {
      "Content-Type": "application/json",
    },
  });
}
這支檔案負責索引並提供可供搜尋的 JSON 資料,包括文章的標題、日期和 slug。現在我們已經有了搜尋數據,接下來建立搜尋結果頁面。
在 src/pages 建立 search.astro 搜尋結果頁面
---
import { SITE_TITLE } from '../consts';
import MainLayout from "../layouts/MainLayout.astro";
---
<MainLayout title={SITE_TITLE}>
  <main>
    <h2 id="searchTitle" class="text-3xl text-default text-center"></h2>
    <ul
      aria-label="Search Results"
      id="searchResults"
      class="py-10 border-y border-default"
    >
    </ul>
  </main>
</MainLayout>
<script>
  import DOMPurify from 'dompurify';
  import { SITE_TITLE } from '../consts';
  let SEARCH_DATA: any;
  const search: HTMLInputElement = document.querySelector('#search')!;
  const searchTitle: Element = document.querySelector('#searchTitle')!;
  const resultsList: Element = document.querySelector('#searchResults')!;
  // functions
  function updateDocumentTitle(search: string) {
    document.title = search ? `${SITE_TITLE} | Search “${search}”` : SITE_TITLE;
  }
  function updateSearchTitle(search: string) {
    const searchText = search ? `Search:${search}` : '';
    searchTitle.textContent = searchText;
  }
  const generateSearchList = (results: any, search: string) => {
    return results
      .map((r: any) => {
        const { title, date, slug } = r;
        const dateAsDate = new Date(date);
        return `<li class="py-4 px-[4vw]">
								<time datetime="${dateAsDate.toISOString()}">
									${dateAsDate.toLocaleDateString('en-us', {
                    year: 'numeric',
                    month: 'short',
                    day: 'numeric',
                  })}
								</time>
								<a href="/blog/${slug}/">${title}</a>
							</li>`;
      })
      .join('');
  };
  async function fetchSearchResults(search: string) {
    if (search?.length === 0) return;
    if (!SEARCH_DATA) {
      try {
        const res = await fetch('/search.json');
        if (!res.ok) {
          throw new Error('Something went wrong…please try again');
        }
        const data = await res.json();
        SEARCH_DATA = data;
      } catch (e) {
        console.error(e);
      }
    }
    const list = SEARCH_DATA.filter((s: any) => {
      return s.title.toLowerCase().includes(search.toLowerCase());
    });
    resultsList!.innerHTML =
      list?.length > 0
        ? generateSearchList(list, search)
        : `
      <div class="border-default py-10 px-[4vw]"><p>目前沒有關於 ${search} 主題的文章哦!</p></div>
      `;
  }
  // event listeners
  window.addEventListener('DOMContentLoaded', () => {
    const urlParams = DOMPurify.sanitize(
      new URLSearchParams(window.location.search).get('q')
    );
    fetchSearchResults(urlParams);
    updateDocumentTitle(urlParams);
    updateSearchTitle(urlParams);
    search.value = urlParams;
    search.focus();
  });
</script>
在這段程式碼中
html 部份
我們在 <main> 標籤中建立了一個用於顯示搜尋結果的容器。
使用 id 屬性讓 js 能抓取得到#searchTitle 用來放置搜尋標題#searchResults,用來放置搜尋結果後的標題
js
SEARCH_DATA 變數,用於儲存搜尋結果最後將 SearchWidget 引入到 src/components/Header.astro,裡面的排板再稍微調整一下
---
import SearchWidget from './SearchWidget.astro';
---
<!-- 略 -->
<nav>
  <h2><a href="/">{SITE_TITLE}</a></h2>
  <div class="internal-links">
    <HeaderLink href="/">Home</HeaderLink>
    <HeaderLink href="/product">Product</HeaderLink>
    <HeaderLink href="/blog/page/1">Blog</HeaderLink>
    <HeaderLink href="/about">About</HeaderLink>
    <HeaderLink href="/contact">Contact</HeaderLink>
  </div>
  <!-- -->
  <div class="flex items-center gap-2">
    <SearchWidget />
    <Theme />
  </div>
  <!--  -->
</nav>
<!-- 略 -->
今天,我們學到了:
JSON 數據,以便搜尋結果的檢索。範例連結:https://stackblitz.com/edit/withastro-astro-xosg7x