Use pagefind for search

This commit is contained in:
Maciej Jur 2023-05-04 20:23:39 +02:00
parent 4ac361e66c
commit f166aab8f7
6 changed files with 440 additions and 239 deletions

View file

@ -5,6 +5,7 @@ import remarkEmoji from 'remark-emoji';
import remarkRuby from './src/utils/ruby';
import mdx from '@astrojs/mdx';
import solid from '@astrojs/solid-js';
import pagefind from 'astro-pagefind';
// https://astro.build/config
@ -27,5 +28,6 @@ export default defineConfig({
integrations: [
mdx(),
solid(),
pagefind(),
]
});

View file

@ -10,13 +10,13 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/mdx": "^0.19.0",
"@astrojs/solid-js": "^2.1.0",
"astro": "^2.3.2",
"@astrojs/mdx": "^0.19.1",
"@astrojs/solid-js": "^2.1.1",
"astro": "^2.4.0",
"astro-pagefind": "^1.1.0",
"dayjs": "^1.11.7",
"leaflet": "^1.9.3",
"leaflet.markercluster": "^1.5.3",
"lunr": "^2.3.9",
"rehype-katex": "^6.0.3",
"rehype-raw": "^6.1.1",
"rehype-stringify": "^9.0.3",
@ -26,7 +26,7 @@
"remark-parse": "^10.0.1",
"remark-rehype": "^10.1.0",
"reveal.js": "^4.5.0",
"solid-js": "^1.7.3",
"solid-js": "^1.7.4",
"unified": "^10.1.2",
"unist-util-visit": "^4.1.2"
},

File diff suppressed because it is too large Load diff

View file

@ -1,23 +1,69 @@
import { createSignal, onMount } from "solid-js";
import dayjs from "dayjs";
import lunr from "lunr";
import { createResource, createSignal, For, onMount } from "solid-js";
interface Pagefind {
search: (query: string) => Promise<PagefindResponse>;
}
interface PagefindResult {
id: string;
data: () => Promise<PagefindDocument>;
}
interface PagefindResponse {
results: PagefindResult[];
}
interface PagefindDocument {
url: string;
excerpt: string;
filters: {
author: string;
};
meta: {
title: string;
image: string;
};
content: string;
word_count: number;
}
async function loadPagefind(): Promise<Pagefind> {
const pf = "/_pagefind/pagefind.js";
return await import(/* @vite-ignore */ pf);
}
function Result(props: { page: PagefindResult }) {
const [data, setData] = createSignal<PagefindDocument>();
onMount(async () => await props.page.data().then(setData))
return (
<>
{data() && (
<a class="c-search__result" href={data()!.url}>
<header class="c-search__header">
<h2 class="c-search__title">{data()!.meta.title}</h2>
{/* <time class="c-search__date" datetime={dayjs().toISOString()}>{dayjs().format("MMM DD, YYYY")}</time> */}
</header>
<div class="c-search__excerpt" innerHTML={data()!.excerpt}></div>
</a>
) || (
<div>Loading...</div>
)}
</>
)
}
const KEY = 'q' as const;
export default function Search() {
const [query, setQuery] = createSignal('');
// Data loaded from server
let index: lunr.Index;
let metadata: any;
async function load() {
const data = await fetch('/search.json').then(r => r.json());
index = lunr.Index.load(data.index);
metadata = data.metadata;
}
// Dynamically calculated from query
const results = () => index?.search(query()) ?? [];
// Search
let pagefind: Pagefind;
const [pages] = createResource(query, async (query: string) => await pagefind.search(query));
// Update URL query and history
let debounce: number;
@ -42,7 +88,8 @@ export default function Search() {
}
onMount(async () => {
await load().then(sync);
pagefind = await loadPagefind();
sync();
window.addEventListener('popstate', sync);
});
@ -55,17 +102,10 @@ export default function Search() {
{query() && (
<section class="c-search__results">
<div>Showing results for "{query()}" ({results().length})</div>
{results().map(result => {
const meta = metadata[result.ref];
const date = dayjs(meta.date);
return (
<a class="c-search__result" href={result.ref}>
<span class="c-search__name">{meta.title}</span>
<time class="c-search__date" datetime={date.toISOString()}>{date.format("MMM DD, YYYY")}</time>
</a>
)
})}
<div>Showing results for "{query()}" ({pages()?.results.length || 0})</div>
{pages() && <For each={pages()!.results}>{(page, i) => (
<Result page={page} />
)}</For>}
</section>
) || (
<div>No results to show yet...</div>

View file

@ -1,31 +0,0 @@
import lunr from 'lunr';
import { getCollection } from 'astro:content';
import type { APIContext } from "astro";
const posts = await Promise.all([
getCollection('posts').then(a => a.map(p => ({...p, slug: `/posts/${p.slug}/`}))),
getCollection('aoc').then(a => a.map(p => ({...p, slug: `/aoc/${p.slug}/`}))),
getCollection('slides').then(a => a.map(p => ({...p, slug: `/slides/${p.slug}/`}))),
])
.then(array => array.flat());
const metadata = posts.reduce(
(acc, next) => (
acc[next.slug] = { title: next.data.title, date: next.data.date },
acc
),
{} as {[key: string]: {title: string, date: Date}}
)
const index = lunr(function() {
this.ref('slug');
this.field('body');
for (const post of posts)
this.add(post);
})
export async function get(_: APIContext) {
return {body: JSON.stringify({ index, metadata })};
}

View file

@ -21,13 +21,12 @@
}
&__result {
display: flex;
flex-direction: row;
padding: 0.5em;
background-color: white;
box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px;
transition: box-shadow linear 100ms;
text-decoration: none;
color: var(--c-text);
&:focus-within,
&:hover {
@ -35,8 +34,25 @@
}
}
&__header {
display: flex;
}
&__title {
font-size: 1.3em;
margin: 0;
}
&__date {
margin-left: auto;
color: rgb(65, 65, 65);
}
&__excerpt {
mark {
background-color: unset;
font-weight: 800;
text-decoration: underline;
color: var(--c-primary-d);
}
}
}