没有框架的博客 UI:搜索、筛选、覆盖链接与目录
没有 React,也没有组件库。博客需要搜索、分类筛选、整卡链接、阅读目录和代码复制,这些都可以用浏览器原生能力完成。
没有 React,也没有组件库。博客需要搜索、分类筛选、整卡链接、阅读目录和代码复制,这些都可以用浏览器原生能力完成。
这个博客的交互层只有一个 main.js。它不负责渲染内容,只负责在已有 HTML 上补行为:打开搜索、过滤卡片、复制代码、跟随目录。
搜索:构建时生成一份小索引
搜索不查数据库。build.mjs 在构建时把标题、摘要、标签、分类和正文片段写进 search-index.json:
[
{
"title": "把封面做成一层会退场的舞台",
"url": "/posts/cover-scroll-ui.html",
"categoryName": "技术笔记",
"tags": ["UI", "CSS"],
"excerpt": "…",
"text": "…"
}
]
用户第一次打开搜索面板时才 fetch 这份 JSON,之后缓存在内存里。没有第三方搜索服务,也没有额外的后端。
排序是一个简单的加权:
if (title.includes(term)) score += 80;
if (tags.includes(term)) score += 42;
if (category.includes(term)) score += 24;
if (excerpt.includes(term)) score += 20;
if (text.includes(term)) score += 5;
标题命中权重最高,正文只做兜底。输入多个词时,要求每个词都有命中,避免“搜到一堆不相关的东西”。
这里踩过一个真实的竞态问题:搜索面板打开后开始加载 JSON,如果用户在加载完成前就输入关键词,索引返回时会把结果清空。修复方式是加载完成后再检查一次当前输入:
loadIndex().then(() => {
if (input.value.trim()) renderSearch(input.value);
else showHint();
});
筛选:把状态放进 URL
文章库的分类和标签不是服务端过滤,而是给每张卡片写 data-category 与 data-tags:
<article class="post-card"
data-category="tech"
data-tags="UI|CSS|性能">
点击分类或标签时:
const params = new URLSearchParams(location.search);
params.set("category", category);
history.replaceState(null, "", `?${params}`);
然后遍历卡片,按分类、标签、关键词三项匹配,切换 hidden。这样刷新页面、复制链接、前进后退都还能保留筛选状态。
标签使用 | 分隔而不是数组字符串,比较时精确匹配,避免“UI”误命中“UI 细节”。
整卡链接:不要嵌套 <a>
最初的文章卡片是一个大 <a>,里面又放了标签 <a>。浏览器遇到嵌套链接时会自动闭合外层 <a>,结果标题、摘要和标签全被甩出卡片,布局直接坏掉。
正确做法是“标题链接 + 覆盖层”:
.post-card {
position: relative;
isolation: isolate;
}
.post-card__title::after {
content: "";
position: absolute;
inset: 0;
z-index: 1;
}
.post-card .tag-row {
position: relative;
z-index: 2;
}
标题本身是链接,::after 覆盖整张卡片;标签放在更高层级,仍然可以单独点击。HTML 里没有嵌套链接,键盘聚焦也只有一个主要入口。
这个模式同样用在首页的精选卡和最新文章列表上。验收时直接检查 document.querySelectorAll("a a").length === 0。
目录:标题 ID 在构建时生成
Markdown 转 HTML 后,构建器会给每个 h2 / h3 加 ID,并收集成目录。阅读时用滚动位置计算当前章节,而不是一次性监听所有标题:
headings.forEach((heading) => {
if (heading.getBoundingClientRect().top <= 140) current = heading.id;
});
右侧目录用 position: sticky 跟随。移动到手机端后,目录变成一条横向滚动的胶囊条,避免在正文前堆一长列链接。
表格与代码块
Markdown 里的表格在手机上很容易撑破布局。两层处理:
.post-layout {
grid-template-columns: minmax(0, 1fr);
}
.prose > *,
.table-wrap {
min-width: 0;
}
.table-wrap {
overflow-x: auto;
}
.table-wrap table {
min-width: 620px;
}
只写 overflow-x: auto 还不够。如果父级网格是 1fr,表格的 min-content 会把整列撑到 1000px 以上。把网格改成 minmax(0, 1fr),再给表格容器明确宽度,才是完整的修复。
代码块则相反:pre 自己横向滚动,不参与页面宽度计算。
复制与反馈
代码复制优先使用 navigator.clipboard,失败时回退到临时 textarea + execCommand。复制链接按钮也走同一套逻辑。
这不是核心功能,但没有它,技术文章的阅读体验会差一截。
结果
- 搜索、分类、标签筛选都不需要后端。
- 整卡链接和标签点击可以共存,嵌套链接为 0。
- 手机端表格在 350px 容器里滚动,页面本身不横向溢出。
- 目录在桌面端跟随,在手机端折叠成横向条。
没有框架也可以有完整的交互,前提是把 HTML 结构、层级和状态边界想清楚。