Skip to content

Commit d14dc01

Browse files
committed
feat: light code theme and Pagefind static search
1 parent 39ae347 commit d14dc01

8 files changed

Lines changed: 299 additions & 1 deletion

File tree

‎astro.config.mjs‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@ export default defineConfig({
1616
integrations: [mdx(), sitemap()],
1717

1818
markdown: {
19+
shikiConfig: {
20+
// 用 themes + defaultColor: false,Shiki 会把配色输出成 CSS 变量
21+
// (--shiki-light 之类)而不是写死的内联 color/background-color。
22+
// 好处是背景色由我们自己的 CSS 决定,不必用 !important 去盖内联样式。
23+
// 将来要加暗色模式,这里补一个 dark 主题、CSS 里加一条媒体查询即可。
24+
themes: { light: 'github-light' },
25+
defaultColor: false,
26+
// 代码不折行,长行横向滚动;折行会打断缩进结构,反而更难读
27+
wrap: false,
28+
},
29+
1930
// Astro 7 的默认 Markdown 管线是 Sätteri(不是 remark/rehype),
2031
// 所以数学公式走它的 math 解析特性 + 自写的 KaTeX 渲染插件,
2132
// 不需要换成 unified,也就不用引入整套 remark 生态。

‎package-lock.json‎

Lines changed: 120 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"scripts": {
99
"dev": "astro dev",
1010
"build": "astro build",
11+
"postbuild": "pagefind --site dist --exclude-selectors \".katex\"",
1112
"preview": "astro preview",
1213
"astro": "astro"
1314
},
@@ -21,5 +22,8 @@
2122
"katex": "0.18.1",
2223
"sharp": "^0.35.0",
2324
"tailwindcss": "^4.3.3"
25+
},
26+
"devDependencies": {
27+
"pagefind": "1.5.2"
2428
}
2529
}

‎src/components/Header.astro‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import HeaderLink from './HeaderLink.astro';
99
<nav class="flex gap-6 text-[0.9375rem]">
1010
<HeaderLink href="/">首页</HeaderLink>
1111
<HeaderLink href="/blog">笔记</HeaderLink>
12+
<HeaderLink href="/search">搜索</HeaderLink>
1213
<HeaderLink href="/about">关于</HeaderLink>
1314
</nav>
1415
</div>

‎src/layouts/BlogPost.astro‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ const { title, description, pubDate, updatedDate } = Astro.props;
1818
<body>
1919
<Header />
2020
<main class="wrapper py-12">
21-
<article>
21+
{/* data-pagefind-body 限定 Pagefind 只索引文章本体。
22+
一旦全站存在这个标记,没标的页面(首页、列表页)就不会进索引,
23+
搜索结果里也就不会出现页头页脚的导航文字。 */}
24+
<article data-pagefind-body>
2225
<header class="mb-10">
2326
<h1 class="text-[1.75rem] leading-snug">{title}</h1>
2427
<p class="mt-3 text-sm text-muted">

‎src/pages/search.astro‎

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
---
2+
import BaseHead from '../components/BaseHead.astro';
3+
import Footer from '../components/Footer.astro';
4+
import Header from '../components/Header.astro';
5+
import { SITE_TITLE } from '../consts';
6+
---
7+
8+
<!doctype html>
9+
<html lang="zh-CN">
10+
<head>
11+
<BaseHead title={`搜索 · ${SITE_TITLE}`} description="站内全文搜索" />
12+
</head>
13+
<body>
14+
<Header />
15+
<main class="wrapper py-12">
16+
<h1 class="text-[1.75rem]">搜索</h1>
17+
18+
<form class="mt-6" role="search" onsubmit="return false">
19+
<label for="q" class="sr-only">搜索关键词</label>
20+
<input
21+
type="search"
22+
id="q"
23+
name="q"
24+
autocomplete="off"
25+
autofocus
26+
placeholder="输入关键词,如 attention、梯度、tokenizer"
27+
class="w-full border-b border-rule bg-transparent py-2 text-lg outline-none focus:border-accent"
28+
/>
29+
</form>
30+
31+
<p id="status" class="mt-4 text-sm text-muted" aria-live="polite"></p>
32+
<ul id="results" class="mt-6"></ul>
33+
</main>
34+
<Footer />
35+
36+
<script>
37+
const input = document.querySelector('#q') as HTMLInputElement;
38+
const status = document.querySelector('#status') as HTMLParagraphElement;
39+
const results = document.querySelector('#results') as HTMLUListElement;
40+
41+
let pagefind: any = null;
42+
let loadFailed = false;
43+
44+
async function loadPagefind() {
45+
if (pagefind || loadFailed) return pagefind;
46+
// 索引由 postbuild 阶段的 pagefind CLI 生成,产物在 dist/pagefind/。
47+
// 路径写成变量并加 @vite-ignore,防止 Vite 在构建时试图解析这个
48+
// 尚不存在的模块而报错。
49+
const modulePath = '/pagefind/pagefind.js';
50+
try {
51+
pagefind = await import(/* @vite-ignore */ modulePath);
52+
await pagefind.init();
53+
} catch {
54+
loadFailed = true;
55+
status.textContent =
56+
'搜索索引不可用。索引在 npm run build 时生成,本地开发请改用 npm run preview 预览。';
57+
}
58+
return pagefind;
59+
}
60+
61+
function render(items: any[], query: string) {
62+
results.replaceChildren();
63+
64+
if (items.length === 0) {
65+
status.textContent = `没有匹配「${query}」的结果。`;
66+
return;
67+
}
68+
69+
status.textContent = `${items.length} 条结果`;
70+
71+
for (const item of items) {
72+
const li = document.createElement('li');
73+
li.className = 'border-b border-rule py-4';
74+
75+
const link = document.createElement('a');
76+
link.href = item.url;
77+
link.textContent = item.meta?.title ?? item.url;
78+
link.className = 'no-underline hover:underline';
79+
li.append(link);
80+
81+
const excerpt = document.createElement('p');
82+
excerpt.className = 'mt-1 text-sm text-muted';
83+
// excerpt 里含 Pagefind 生成的 <mark> 高亮标签,内容源自本站自己的
84+
// Markdown,不是外部输入,所以这里用 innerHTML 是安全的。
85+
excerpt.innerHTML = item.excerpt;
86+
li.append(excerpt);
87+
88+
results.append(li);
89+
}
90+
}
91+
92+
async function runSearch(query: string) {
93+
const trimmed = query.trim();
94+
95+
// 同步地址栏,方便把搜索结果直接分享或收藏
96+
const url = new URL(window.location.href);
97+
if (trimmed) {
98+
url.searchParams.set('q', trimmed);
99+
} else {
100+
url.searchParams.delete('q');
101+
}
102+
window.history.replaceState({}, '', url);
103+
104+
if (!trimmed) {
105+
results.replaceChildren();
106+
status.textContent = '';
107+
return;
108+
}
109+
110+
const pf = await loadPagefind();
111+
if (!pf) return;
112+
113+
status.textContent = '搜索中…';
114+
const search = await pf.search(trimmed);
115+
// 只取前 20 条,逐条拉取详情
116+
const items = await Promise.all(search.results.slice(0, 20).map((r: any) => r.data()));
117+
render(items, trimmed);
118+
}
119+
120+
let timer: ReturnType<typeof setTimeout>;
121+
input.addEventListener('input', () => {
122+
clearTimeout(timer);
123+
timer = setTimeout(() => runSearch(input.value), 180);
124+
});
125+
126+
// 支持直接访问 /search?q=xxx
127+
const initial = new URL(window.location.href).searchParams.get('q');
128+
if (initial) {
129+
input.value = initial;
130+
runSearch(initial);
131+
}
132+
</script>
133+
</body>
134+
</html>

‎src/plugins/satteri-katex.mjs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ export function satteriKatex(userOptions = {}) {
3131
// 迁移过来的笔记里难免有非标准写法,别为此刷屏
3232
strict: 'ignore',
3333
});
34+
35+
// 注意:公式需要排除出 Pagefind 索引(否则同一条公式会被抓三遍——
36+
// 可见字形、MathML 文本、<annotation> 里的 LaTeX 原文),
37+
// 但这件事不在这里做。实测往这段 HTML 里插 data-pagefind-ignore
38+
// 无效:Sätteri 把 { raw } 字符串重新解析时会丢掉无值属性。
39+
// 所以改在 package.json 的 postbuild 里用 pagefind --exclude-selectors 处理。
3440
return { raw: html, mdxExpressions: false };
3541
} catch (error) {
3642
// 不让一条写错的公式炸掉整个构建,降级成等宽原文。

‎src/styles/global.css‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,18 @@
175175
font-family: inherit;
176176
}
177177

178+
/* Shiki 以 defaultColor: false 输出,配色只存在于 --shiki-light 变量里,
179+
需要这条规则把它取出来用。背景色不取主题的纯白,改用页面自己的浅底色,
180+
让代码块和行内代码在暖白纸面上保持一致。 */
181+
.prose pre.astro-code,
182+
.prose pre.astro-code span {
183+
color: var(--shiki-light);
184+
}
185+
186+
.prose pre.astro-code {
187+
background-color: var(--color-fill);
188+
}
189+
178190
.prose img {
179191
display: block;
180192
max-width: 100%;
@@ -252,6 +264,13 @@
252264
}
253265
}
254266

267+
/* 搜索结果里 Pagefind 生成的关键词高亮 */
268+
mark {
269+
background-color: var(--color-fill);
270+
color: var(--color-ink);
271+
box-shadow: inset 0 -0.4em 0 color-mix(in srgb, var(--color-accent) 18%, transparent);
272+
}
273+
255274
.sr-only {
256275
position: absolute;
257276
width: 1px;

0 commit comments

Comments
 (0)