Статические сайты не имеют базы данных, к которой мы можем запрашивать наши данные. Мы создадим эффективный компонент поиска Astro с fuse.js для работы с нашими файлами уценки.

Реализовать функцию поиска в полнофункциональном приложении сложно. Но сделать это на статическом сайте может показаться невозможным. В конце концов, у вас нет базы данных для запросов, верно?

К счастью, у веб-фреймворка Astro есть API, с помощью которого вы можете получить нужные файлы уценки или MDX. После получения всех данных мы сможем выполнить поиск по конкретному запросу, предоставленному нашими пользователями.

Вот демонстрация того, что мы собираемся построить.

Давайте создадим компонент поиска Astro, используя React и fuse.js.

Примечание.

Исходный код этого проекта доступен в этом репозитории GitHub.

Создание проекта астроблога

Давайте начнем с создания проекта Astro. Я составил подробное руководство о том, как начать проект Astro, если у вас есть какие-либо вопросы об этом процессе.

Я буду использовать рекомендуемый стартовый шаблон от Astro.

npm create astro@latest
✔ Where would you like to create your new project? … Astro-search-component
✔ How would you like to setup your new project? › a few best practices (recommended)
...

Я добавил интеграцию MDX Astro, чтобы иметь возможность использовать файлы с расширениями .md и .mdx, но если вы используете только простые файлы уценки, в этом нет необходимости. Запустите следующее в корне вашего проекта.

npx astro add mdx

Если вы сказали «да» каждому варианту, модифицированный инструмент CLI автоматически изменил ваш файл конфигурации Astro.

Измените файл src/pages/index.astro, чтобы он выглядел следующим образом. Эта страница должна быть доступна под localhost:3000.

---
import Layout from '../layouts/Layout.astro';
const allPosts = await Astro.glob('../posts/*.{md,mdx}');
const posts = allPosts.filter((post) => !post.frontmatter.draft && post.frontmatter.slug);
---
<Layout title="MyBlog">
 <h1>Welcome to my Blog</h1>
</Layout>

Компонент Layout предоставляется стартовым шаблоном Astro. Если у вас его нет, создайте его под именем файла src/layouts/Layout.astro или просто поместите содержимое в обычную структуру HTML.

Пока не беспокойтесь о стилях. Позже в этом уроке мы применим Tailwind CSS.

После этого создайте страницу Astro для динамического отображения сообщений в блоге.

Создайте файл с именем src/pages/[slug].astro. Квадратные скобки означают, что для отображения страницы мы должны передать в URL-адрес параметр, называемый slug. В этом случае URL-адрес страницы любых сообщений будет localhost:3000/[slug]/.

---
import Layout from '../layouts/Layout.astro';
export async function getStaticPaths() {
 const allPosts = await Astro.glob('../posts/*{md,mdx}');
 const posts = allPosts.filter((post) => !post.frontmatter.draft && post.frontmatter.slug);
 return posts.map((post) => ({
  params: {
   slug: post.frontmatter.slug,
  },
  props: { post },
 }));
}
const { post } = Astro.props;
---
<Layout title={post.frontmatter.title}>
 <h2>{post.frontmatter.title}</h2>
 <p>{post.frontmatter.description}</p>
 <post.Content />
</Layout>

Обратите внимание, как мы используем функцию getStaticPaths() для динамического создания путей к нашим сообщениям.

Параметр представляет собой поле slug, которое мы должны указать вручную. Вы можете автоматизировать это, применив функцию slugify к именам файлов сообщений, но это выходит за рамки данного руководства.

Теперь давайте создадим несколько сообщений с разными заголовками и описаниями внутри каталога src/posts/ (вы должны создать эту папку).

Исходный код проекта до этого момента, включая фиктивные посты, доступен в этой ветке GitHub.

Создайте компонент Astro Search

Во-первых, мы собираемся создать файл Astro src/components/SearchBar.astro, который извлекает все сообщения и отправляет их в качестве реквизита компоненту поиска React, который мы позже создадим в той же папке компонентов.

Это похоже на создание клиента, использующего API, а затем создание самого API.

---
import Search from './Search';
const allPosts = await Astro.glob('../posts/*.{md,mdx}');
const posts = allPosts.filter((post) => !post.frontmatter.draft && post.frontmatter.slug);
---
<Search client:load searchList={posts} />

Мы фильтруем сообщения, чтобы они не были черновиками, и у них был определенный ярлык. Затем мы передаем этот список сообщений компоненту React, который мы собираемся создать следующим.

Как видите, это довольно простой клиент, а значит, вся логика опирается на компонент React.

Создание компонента React Search

Прежде чем делать какой-либо код, давайте добавим зависимости в наш проект.

Во-первых, добавьте интеграцию React из @astrojs/react и библиотеку нечеткого поиска fuse.js, которую мы будем использовать для поиска в файлах уценки.

Подробнее о нечетком поиске или более формально приближенном сопоставлении строк можно прочитать в этой статье.

В корне проекта запустите:

npx astro add react
npm i --save fuse.js

Теперь создайте файл .jsx с именем Search.jsx внутри папки компонентов, который будет содержать компонент функции React.

В верхней части этого файла импортируйте Fuse, API fuse.js и хук useState(), который позволяет нам отслеживать состояние (объект, содержащий данные о компоненте) нашего поискового ввода.

import Fuse from 'fuse.js';
import { useState } from 'react';

Задайте опции для объекта Fuse со словарем.

// Configs fuse.js
// https://fusejs.io/api/options.html
const options = {
	keys: ['frontmatter.title', 'frontmatter.description', 'frontmatter.slug'],
	includeMatches: true,
	minMatchCharLength: 2,
	threshold: 0.5,
};

Теперь давайте создадим наш компонент функции поиска. Он возьмет список поиска в качестве опоры.

function Search({ searchList }) {
	// Following code
}
export default Search;

Внутри этой функции используйте хук useState() с пустым начальным значением, чтобы отслеживать состояние переменной query (ввод пользователя).

const [query, setQuery] = useState('');

Создайте объект Fuse с исходным списком поиска и параметрами, которые мы определили выше.

const fuse = new Fuse(searchList, options);

Теперь определите сообщения, которые мы собираемся отображать в строке поиска, используя метод search() объекта Fuse.

// Set a limit to the posts: 5
const posts = fuse
	.search(query)
	.map((result) => result.item)
	.slice(0, 5);

Этот метод поиска возвращает список результатов в соответствии с запросом. Каждый результат имеет объект item, содержащий данные найденного элемента, и refIndex. Поскольку мы хотим получить данные только из элемента, мы преобразуем объект результата, чтобы получить только элемент.

Функция slice() возвращает только первые 5 результатов — самые точные результаты. Вы можете изменить его в соответствии с вашими потребностями.

Теперь создайте функцию-обработчик handleOnSearch для изменения значения запроса в соответствии с вводом пользователя.

function handleOnSearch({ target = {} }) {
	const { value } = target;
	setQuery(value);
}

Мы передадим эту функцию HTML-тегу input.

Примечание.

Вы можете узнать больше об обработке форм в официальной React Docs.

Наконец, верните метку и текстовое поле ввода с query как value и handleOnSearch в качестве обработчика события onChange.

return (
	<>
		<label>Search</label>
		<input type="text" value={query} onChange={handleOnSearch} placeholder="Search posts" />
		{query.length > 1 && (
			<p>
				Found {posts.length} {posts.length === 1 ? 'result' : 'results'} for '{query}'
			</p>
		)}
		<ul>
			{posts &&
				posts.map((post) => (
					<li>
						<a href={`/${post.frontmatter.slug}`}>{post.frontmatter.title}</a>
						{post.frontmatter.description}
					</li>
				))}
		</ul>
	</>
);

Файл src/components/Search.jsx должен выглядеть так.

import Fuse from 'fuse.js';
import { useState } from 'react';
// Configs fuse.js
// https://fusejs.io/api/options.html
const options = {
	keys: ['frontmatter.title', 'frontmatter.description', 'frontmatter.slug'],
	includeMatches: true,
	minMatchCharLength: 2,
	threshold: 0.5,
};
function Search({ searchList }) {
	// User's input
	const [query, setQuery] = useState('');
        const fuse = new Fuse(searchList, options);
         // Set a limit to the posts: 5
         const posts = fuse
          .search(query)
          .map((result) => result.item)
          .slice(0, 5);

         function handleOnSearch({ target = {} }) {
          const { value } = target;
          setQuery(value);
         }
         return (
          <>
           <label>Search</label>
           <input type="text" value={query} onChange={handleOnSearch} placeholder="Search posts" />
           {query.length > 1 && (
            <p>
             Found {posts.length} {posts.length === 1 ? 'result' : 'results'} for '{query}'
            </p>
           )}
           <ul>
            {posts &&
             posts.map((post) => (
              <li>
               <a href={`/${post.frontmatter.slug}`}>{post.frontmatter.title}</a>
               {post.frontmatter.description}
              </li>
             ))}
           </ul>
          </>
 );
}
export default Search;

Поздравляем, теперь у вас есть полнофункциональная панель поиска на вашем сайте!

Вот демонстрация компонента, который у нас есть на данный момент.

Примечание

Вышеупомянутый компонент получает глобальные стили с моего сайта, поэтому темный режим все еще применяется.

Если вы хотите увидеть, как сейчас выглядит весь проект, загляните в эту ветвь GitHub.

Применение стилей CSS Tailwind

Давайте закончим этот проект, добавив стили Tailwind (мой любимый CSS-фреймворк) к компоненту панели поиска.

Для этого установите интеграцию с Astro Tailwind и плагин типографики, чтобы изменить стили на отображаемых страницах с уценкой, таких как сообщения.

npx astro add tailwind
npm i --save @tailwindcss/typography

В сгенерированный файл tailwind.config.cjs в корне вашего проекта добавьте следующее в список плагинов.

// /tailwind.config.cjs
/** @type {import('tailwindcss').Config} */
module.exports = {
	content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
	theme: {
		extend: {},
	},
	plugins: [require('@tailwindcss/typography')],
};

Вот файл src/pages/index.astro.

<Layout title="MyBlog">
	<div class="py-10 lg:py-16">
		<h1
			class="text-5xl lg:text-7xl uppercase font-bold bg-clip-text text-transparent bg-gradient-to-tr from-blue-500 to-green-500 text-center"
		>
			Welcome to my Blog
		</h1>
	</div>
	<div class="max-w-3xl mx-auto">
		<SearchBar />
	</div>
</Layout>

Теперь, чтобы стилизовать панель поиска, мы должны добавить атрибут className к тегам внутри оператора return.

Я также буду использовать иконку поиска из Iconify, одной из самых больших коллекций иконок с открытым исходным кодом.

// src/components/Search.jsx -> Search
return (
	<div>
		<label htmlFor="search" className="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">
			Search
		</label>
		<div className="relative">
			<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
				<svg
					xmlns="http://www.w3.org/2000/svg"
					className="icon icon-tabler icon-tabler-search"
					width={24}
					height={24}
					viewBox="0 0 24 24"
					strokeWidth="2"
					stroke="currentColor"
					fill="none"
					strokeLinecap="round"
					strokeLinejoin="round"
				>
					<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
					<circle cx={10} cy={10} r={7}></circle>
					<line x1={21} y1={21} x2={15} y2={15}></line>
				</svg>
			</div>
			<input
				type="text"
				id="search"
				value={query}
				onChange={handleOnSearch}
				className="block w-full p-4 pl-10 text-sm 
                                text-gray-900 
                               border border-gray-300
                               rounded-lg bg-gray-50
                                focus:outline-none
                               focus:ring-blue-500
                               focus:border-blue-500"
    placeholder="Search for anything..."
   />
  </div>
  {query.length > 1 && (
   <div className="my-4">
    Found {posts.length} {posts.length === 1 ? 'result' : 'results'} for '{query}'
   </div>
  )}
  <ul className="list-none">
   {posts &&
    posts.map((post) => (
     <li className="py-2">
      <a
       className="text-lg text-blue-700 hover:text-blue-900 hover:underline underline-offset-2"
       href={`/${post.frontmatter.slug}`}
      >
       {post.frontmatter.title}
      </a>
      <p className="text-sm text-gray-800">{post.frontmatter.description}</p>
     </li>
    ))}
  </ul>
 </div>
);

Вот как должна выглядеть главная страница.

Наконец, измените файл src/pages/[slug].astro, чтобы статьи выглядели красивее.

<Layout title={post.frontmatter.title}>
	<div class="pb-12 mx-auto max-w-3xl prose prose-md prose-headings:font-bold prose-a:text-blue-600">
		<h2 class="text-center text-5xl pt-12 pb-3">{post.frontmatter.title}</h2>
		<p class="text-center text-lg text-gray-600 pb-4">{post.frontmatter.description}</p>
		<post.Content />
	</div>
</Layout>

Класс prose позволяет нам добавлять стили Tailwind к содержимому HTML, которое мы не контролируем — например, HTML, отображаемый из файлов уценки.

Теперь, когда вы посещаете статью, у вас будет следующая страница.

Краткое содержание

В этом руководстве вы узнали, как создать компонент Astro Search с помощью React и fuse.js.

Вы использовали Astro API для получения всех опубликованных сообщений, передавали их в виде списка поиска функциональному компоненту React и создавали объект fuse.js для поиска сообщений, которые пользователи вводят в поле ввода.

Наконец, вы установили интеграцию с Tailwind, которая позволила вам стилизовать ваш сайт без написания собственного CSS.

Если у вас есть какие-либо отзывы об этом уроке, пожалуйста, дайте мне знать!