feat(search): 站内搜索/RSS/sitemap/Artalk 懒挂载
- astro-pagefind:构建期索引;SearchDialog 首开懒加载 + 200ms 防抖,
入口=Header 钮 + 键盘 /(P0 键位集补全)
- @astrojs/rss:posts 全量(draft 排除),双语分流留 Phase 3
- @astrojs/sitemap:sitemap-index 产出
- ArtalkComments:IntersectionObserver 进入视口才拉 Artalk.js,
pageKey=/archives/{slug}/(中英共键,评论不分裂)
验证: pnpm astro build exit 0;dist/pagefind 目录 + rss.xml +
sitemap-index.xml 三件断言过
Spec: 2026-10-24_add-blog-skeleton
Co-authored-by: GLM-5.3
This commit is contained in:
1 parent
945a9ee610
commit
4694428614
9 files changed
+449
-13
No files matched your search
@@ -0,0 +1,101 @@
|
||||
---
|
||||
// 站内搜索(Pagefind):首次打开才懒加载索引(spectre 模式,见调研报告 §4c)
|
||||
// 入口:Header 搜索钮 + 键盘 `/`
|
||||
---
|
||||
|
||||
<dialog id="search-dialog" class="search-dialog">
|
||||
<input id="search-input" type="search" placeholder="搜索文章…(Esc 关闭)" autocomplete="off" />
|
||||
<ul id="search-results" class="results" aria-live="polite"></ul>
|
||||
<p class="search-hint">索引由 Pagefind 构建期生成 · 静态站零后端</p>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
const dlg = document.getElementById('search-dialog') as HTMLDialogElement;
|
||||
const input = document.getElementById('search-input') as HTMLInputElement;
|
||||
const list = document.getElementById('search-results')!;
|
||||
type PagefindLike = {
|
||||
search: (q: string) => Promise<{ results: { data: () => Promise<{ url: string; excerpt: string; meta: { title: string } }> }[] }>;
|
||||
};
|
||||
let pf: PagefindLike | null = null;
|
||||
let loadStarted = false;
|
||||
|
||||
async function ensureIndex() {
|
||||
if (loadStarted) return;
|
||||
loadStarted = true;
|
||||
pf = (await import(/* @vite-ignore */ '/pagefind/pagefind.js')) as unknown as PagefindLike;
|
||||
}
|
||||
|
||||
function open() {
|
||||
dlg.showModal();
|
||||
input.value = '';
|
||||
list.innerHTML = '';
|
||||
void ensureIndex();
|
||||
input.focus();
|
||||
}
|
||||
|
||||
document.querySelector('[data-search-trigger]')?.addEventListener('click', open);
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const inField = document.activeElement instanceof HTMLElement &&
|
||||
(document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA');
|
||||
if (e.key === '/' && !inField) { e.preventDefault(); open(); }
|
||||
});
|
||||
|
||||
let timer: number | undefined;
|
||||
input.addEventListener('input', () => {
|
||||
clearTimeout(timer);
|
||||
timer = window.setTimeout(async () => {
|
||||
if (!pf) return;
|
||||
const q = input.value.trim();
|
||||
if (!q) { list.innerHTML = ''; return; }
|
||||
const res = await pf.search(q);
|
||||
list.innerHTML = '';
|
||||
for (const r of res.results.slice(0, 8)) {
|
||||
const d = await r.data();
|
||||
const li = document.createElement('li');
|
||||
const a = document.createElement('a');
|
||||
a.href = d.url;
|
||||
a.className = 'tap';
|
||||
a.innerHTML = `<strong>${d.meta.title}</strong><span>${d.excerpt}</span>`;
|
||||
li.appendChild(a);
|
||||
list.appendChild(li);
|
||||
}
|
||||
if (!list.children.length) list.innerHTML = '<li class="empty">无结果</li>';
|
||||
}, 200);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.search-dialog {
|
||||
margin-top: 12vh;
|
||||
width: min(560px, calc(100vw - 32px));
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: var(--bg);
|
||||
color: var(--ink-2);
|
||||
padding: var(--space-3);
|
||||
}
|
||||
.search-dialog::backdrop { background: rgba(0, 0, 0, 0.45); }
|
||||
input[type='search'] {
|
||||
width: 100%;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-soft);
|
||||
font-size: 15px;
|
||||
}
|
||||
.results { list-style: none; padding: 0; margin: var(--space-2) 0 0; max-height: 50vh; overflow-y: auto; }
|
||||
.results a {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.results a:hover { background: var(--bg-soft); }
|
||||
.results strong { color: var(--ink-1); font-size: 14px; }
|
||||
.results span { font-size: 12px; color: var(--ink-3); }
|
||||
.results a span :global(mark) { background: var(--accent-soft); }
|
||||
.empty { padding: var(--space-3); font-size: 13px; color: var(--ink-3); }
|
||||
.search-hint { margin-top: var(--space-2); font-size: 11px; color: var(--ink-4); text-align: center; font-family: var(--font-mono); }
|
||||
</style>
|
||||
Reference in new issue
Block a user