Astro文章搜索,创建简单的搜索组件
Astro文章搜索,创建简单的搜索组件
需要为整个站点的文字提供一个简单的搜索功能,主要是从文章的title和tag和发布时间等检索。
设想是根据全部文章的json文件检索对应的entry.data内容。实现可以参考Api端点和js简单正则匹配。
利用静态端点APi生成我们搜索的数据源json
在pages文件夹下创建对应的content.json.ts文件。当我们生成dist文件时,会自动生成一个content.json文件。
具体内容如下:
import { getAllPosts } from '@assets/ts/utils';
import { getCollection } from 'astro:content';
export const GET = async () => {
const blogEntries = await getAllPosts();
const content = blogEntries.map((entry) => ({
slug: entry.slug,
data: {
...entry.data,
pubDate: entry.data.pubDate.toLocaleDateString("zh-cn", {
year: "numeric",
month: "2-digit",
day: "numeric",
})
},
}));
return new Response(JSON.stringify({ posts: content }));
};
后续会生成如下:
{"posts": [
{
"slug": "2024-02",
"data": {
"title": "Astro实现分页",
"pubDate": "2024/06/11",
"author": "霁",
"heroImage": null,
"slug": "2024-02",
"categories": [
"JavaScript"
],
"tags": [
"Astro"
]
}
},
{
"slug": "2024-01",
"data": {
"title": "从Jekyll迁移到Astro要做什么?",
"pubDate": "2024/06/10",
"author": "霁",
"heroImage": null,
"slug": "2024-01",
"categories": [
"JavaScript"
],
"tags": [
"Astro"
]
}
}
]
}
创建简单的搜索框组件
需要包括input输入框,结果显示区域,具体样式可自定义
---
import { config } from '@assets/ts/consts';
---
<search>
<span>
<svg class='size-5' viewBox='0 0 24 24' fill='none'>
<path
d='M21 21L15 15M17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10Z'
stroke='currentColor'
stroke-width='2'
stroke-linecap='round'
stroke-linejoin='round'>
</path>
</svg>
</span>
<input
type='text'
id='search-input'
data-json={config.simple_search.json}
placeholder={config.simple_search.placeholder}
/>
<ul id='results-container' class='divide-y bg-site-light dark:bg-site-dark'>
<!-- 这里插入搜索结果列表项 -->
</ul>
</search>
<script>
import { SimpleSearch } from '@assets/ts/utils';
// 初始化搜索功能
document.addEventListener('astro:page-load', function () {
SimpleSearch({
searchInput: document.getElementById('search-input'),
resultsContainer: document.getElementById('results-container'),
json: `${document.getElementById('search-input')?.dataset.json}` // 你的博客文章数据
}).init();
});
</script>
<style>
search {
@apply relative flex h-full w-full justify-center max-sm:px-5;
}
span {
@apply absolute inset-y-0 left-0 flex items-center max-sm:left-5;
}
input {
@apply my-1 h-12 w-full flex-auto border-b-2 border-transparent bg-inherit pl-10 text-inherit focus:border-b-primary-light focus:outline-none max-sm:pl-8;
}
ul {
@apply absolute inset-x-0 top-full flex max-h-72 flex-col overflow-y-auto shadow;
}
</style>
实现对应的SimpleSearch
需要根据输入框的输入去匹配content.json,具体做法就是类似fetch后台的形式。
可以参考如下
const SimpleSearch = (options) => {
const defaults = {
searchInput: null,
resultsContainer: null,
json: null,
searchResultTemplate: "",
noResultsText: `没有找到匹配的文章`,
limit: 10,
fuzzy: false,
};
// 合并用户提供的选项和默认选项
const settings = Object.assign({}, defaults, options);
const regexPost = (data, query) => {
const regex = new RegExp(query.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"), "gi");
const results: any[] = [];
data.forEach((post) => {
const { data } = post;
if (
(settings.fuzzy &&
(data.title.match(regex) || data.tags.join(",").match(regex) || data.pubDate.match(regex))) ||
(!settings.fuzzy &&
(data.title.toLowerCase().includes(query.toLowerCase()) ||
data.tags.join(",").toLowerCase().includes(query.toLowerCase()) ||
data.pubDate.toLowerCase().includes(query.toLowerCase())))
) {
results.push(post);
}
});
renderResults(results.slice(0, settings.limit));
};
// 执行搜索的函数
async function search(query) {
const results: any[] = [];
if (query === "") {
settings.resultsContainer.innerHTML = "";
return;
}
// 尝试从 localStorage 中获取缓存的 JSON 数据
const cachedData = sessionStorage.getItem("searchCache");
if (cachedData) {
// 解析缓存数据
const parsedData = JSON.parse(cachedData);
// 检查数据是否已过期
const now = new Date().getTime();
const expirationTime = parsedData.expirationTime || now;
if (now < expirationTime) {
results.push(...parsedData.data);
}
}
if (results.length > 0) {
regexPost(results, query);
} else {
const content = await fetch(settings.json);
const { posts } = await content.json();
// 设置数据的有效期
const expirationTime = new Date(Date.now() + 1000 * 60 * 60 * 24).getTime(); // 设置有效期为 24 小时
// 更新缓存数据并存储
const updatedData = {
data: posts,
expirationTime: expirationTime,
};
sessionStorage.setItem("searchCache", JSON.stringify(updatedData));
regexPost(posts, query);
}
}
// 渲染搜索结果的函数
function renderResults(results) {
const container = settings.resultsContainer;
container.innerHTML = "";
if (results.length === 0) {
container.innerHTML = `${settings.noResultsText}`;
} else {
results.forEach((result) => {
let tagHTML = "";
// // 遍历标签数组,为每个标签生成 HTML 元素
const { slug, data } = result;
const { title, pubDate, tags } = data;
tags.forEach((tag) => {
tagHTML += `<a href="/tags/${tag}" class="tag">#${tag}</a>`;
});
container.innerHTML += `<li class="flex flex-col flex-auto justify-between p-5 space-y-2">
<a class="inline-block" href="/blog/${slug}/" title="${title}">
<h2>${title}</h2>
</a>
<div class="flex justify-between items-center w-full ">
<div class="flex justify-around space-x-2">${tagHTML}</div>
<time>${pubDate}</time>
</div>
</li>
`;
});
}
}
// 监听整个文档的点击事件
document.addEventListener("click", function (event) {
const inputElement = settings.searchInput; // 替换成你的输入框的实际 ID
const resultsContainer = settings.resultsContainer; // 替换成你搜索结果容器的实际 ID
// 检查点击事件的目标是否是输入框以外的元素,并且搜索结果容器是可见的
if (
event.target !== inputElement &&
!inputElement.contains(event.target) &&
event.target !== resultsContainer &&
!resultsContainer.contains(event.target)
) {
// 销毁搜索结果
destroyResults();
}
});
// 销毁搜索结果的函数
function destroyResults() {
const resultsContainer = settings.resultsContainer; // 替换成你搜索结果容器的实际 ID
resultsContainer.innerHTML = ""; // 清空搜索结果容器的内容,或者隐藏搜索结果容器
}
function debounce(func, delay) {
let debounceTimer;
return function (...args) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => func.apply(this, args), delay);
};
}
const handleInputDebounced = debounce(search, 300);
// 初始化搜索功能
function init() {
const input = settings.searchInput;
if (!input) return;
input.addEventListener("input", function (event) {
handleInputDebounced(event.target.value);
});
}
// 返回公共方法
return {
init: init,
search: search,
};
}