与Obsidian同步的博客搭建——前端美化
与Obsidian同步的博客搭建——前端美化
前两篇把写作链路和页面基础打通以后,这个博客其实已经能用了。但“能用”和“愿不愿意天天打开”是两回事。对个人博客来说,前端最重要的任务不是堆功能,而是让页面有识别度、读起来舒服,交互也别拖后腿。
这一篇只拆前端部分。我会把最后落地的几块拆开说,包括紫色暗色主题、代码块增强、图片 Lightbox、移动端 TOC 悬浮球、首页分类卡片,以及 htmx 搜索交互。整套方案坚持一个原则,能用原生 CSS 和原生 JavaScript 解决的问题,就别额外引入一层框架。
设计方案
先说整体思路。首页不是传统的“标题堆叠列表”,而是 Hero 区 + 内容流的结构。最上面放一块视觉强一点的 Hero 区域,背景是一张风景图,外面压一层紫色渐变遮罩,下面再接分类入口、文章列表和分页。这样做的好处很直接,用户第一次打开时就能立刻记住这个站的气质,而不是看到一页默认样式。
配色我从一开始就定成了紫色系,而且直接以暗色模式为主。原因不复杂,这个博客里代码内容很多,我自己也经常在晚上写和看,暗底更稳,紫色又能把站点和一般的黑灰技术博客区分开。Hero、按钮高亮、链接、卡片边框发光,全都围绕同一组紫色变量展开,这样整站会更统一。
技术选型上,我没有上任何前端框架。样式全部写在 style.css,交互全部写在 app.js,只在搜索和评论这种局部增强场景用了少量 htmx。原生方案的优点是依赖少、体积小、性能直接,出了问题也容易顺着 DOM 和样式表定位。代价也很明显,细节都得自己管,CSS 命名、层级控制、响应式断点、交互状态,全都没有人替你兜底。我的取舍是,对个人博客来说,这个成本完全能接受。
紫色主题 CSS (style.css)
这个项目的 style.css 现在已经是 1100+ 行的手写 CSS。听起来很多,但它不是无序堆出来的,而是按基础变量、布局、组件、文章页、目录、响应式几个层次往下铺。先把颜色系统定住,后面每一块样式才不会飘。
我先在 :root 里定义了一套紫色系调色板,把背景、面板、文字、边框和高亮色全都抽成变量:
:root {
--bg-0: #0b0912;
--bg-1: #151022;
--bg-2: #201830;
--panel: rgba(28, 21, 45, 0.82);
--panel-strong: rgba(39, 28, 63, 0.92);
--border: rgba(180, 151, 255, 0.16);
--text-1: #f4efff;
--text-2: #cfc3ea;
--text-3: #9e90bc;
--primary: #9d7bff;
--primary-strong: #b89cff;
--accent: #7a5cff;
--shadow: 0 18px 45px rgba(8, 5, 18, 0.42);
}
有了这组变量以后,后面的维护会轻松很多。比如你想把主色调从偏冷紫改成偏粉紫,不需要满文件搜索十几个十六进制颜色,直接改变量就行。
全局样式上,我把页面背景做成了深色渐变,再叠一点大范围的径向光晕,让纯暗色不至于死黑。正文排版保持克制,行高和段落间距都偏宽松,代码和长文阅读会舒服不少。导航栏则固定在顶部,配合 backdrop-filter 做半透明毛玻璃效果,滚动时既能保持层次感,也不会把正文完全压死。
Hero 区域是首页最显眼的部分。这里的核心不是“图铺满就完事”,而是图、遮罩、标题动画三者一起服务首屏气氛。背景图上先盖一层深色渐变,保证白字有足够对比度,再给主标题加一个轻微的上浮淡入动画,让页面刚打开时不至于太硬。这个动画要轻,别做成展示站那种夸张转场,不然读技术博客会显得很吵。
文章列表部分我用的是卡片布局。每张卡片有独立的背景、边框和阴影,悬停时只做很小的位移和发光增强,这样能保留反馈,又不会让页面看起来像在跳。类似下面这种写法就够用:
.post-card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 22px;
box-shadow: var(--shadow);
transition: transform 0.24s ease, border-color 0.24s ease;
}
.post-card:hover {
transform: translateY(-4px);
border-color: rgba(184, 156, 255, 0.35);
}
文章页本身是 CSS 里最花时间的部分。标题、段落、列表、引用、分割线、代码块、表格、行内代码,都得单独打磨。尤其是代码块和引用,如果只用默认样式,整篇文章会立刻掉档次。桌面端的 TOC 固定在右侧边栏,方便长文跳转。移动端则完全换交互,不硬塞一个窄侧栏。最后再靠媒体查询做响应式,把间距、字号、卡片列数和目录行为按断点逐层收紧,这样同一套样式才能在手机上真正可读。
如果你问我原生 CSS 值不值得写到这个程度,我的答案是值得。它的缺点是前期慢,规范得自己立。可一旦你把变量、组件和层级关系捋顺了,后面加页面反而很稳,而且最终输出没有多余运行时,这对个人博客很合适。
代码块增强 (app.js)
app.js 现在有 570+ 行,主要做两件事,一是给页面加交互,二是给 Markdown 渲染出来的基础 HTML 做增强。代码块增强就属于第二类。
Markdown 渲染后的代码块,默认通常只是 pre > code。这当然能看,但信息密度不够。技术博客里,语言标签和复制按钮几乎是刚需,所以我在页面加载后遍历所有 pre > code 元素,动态补了一层 UI。这样做的好处是不用改 Markdown 内容,也不用在模板里为每种语言写分支。
核心逻辑大概是这样:
document.querySelectorAll('pre > code').forEach((code) => {
const pre = code.parentElement;
const languageClass = [...code.classList].find((name) =>
name.startsWith('language-')
);
const language = languageClass
? languageClass.replace('language-', '')
: 'text';
const toolbar = document.createElement('div');
toolbar.className = 'code-toolbar';
toolbar.innerHTML = `
<span class="code-lang">${language}</span>
<button class="code-copy" type="button">复制</button>
`;
pre.classList.add('code-block');
pre.prepend(toolbar);
toolbar.querySelector('.code-copy').addEventListener('click', async () => {
await navigator.clipboard.writeText(code.textContent || '');
const button = toolbar.querySelector('.code-copy');
button.textContent = '已复制';
setTimeout(() => {
button.textContent = '复制';
}, 1200);
});
});
语言标签放在左上角,复制按钮放在右上角,视觉上和代码块头部融成一条工具栏。这里我没有把按钮做得太抢眼,只给了轻微描边和悬停反馈,因为它是辅助操作,不该抢正文的注意力。
原生 JS 做这件事的好处是非常直白,遍历、创建节点、绑定事件,逻辑一眼能看完。缺点也有,像复制失败提示、旧浏览器兼容、按钮状态复原这些细节都得自己写。对博客来说这不算大问题,因为交互规模很小,可控范围内自己处理反而更安心。
图片 Lightbox
文章里的图片如果只是平铺在正文里,体验会很碎。所以我先给正文图片统一加了圆角、阴影和悬停反馈,让它默认就像一张卡片。点击之后再进入 Lightbox,全屏查看更合适。
实现上我没有接第三方库,而是自己在页面里生成一个覆盖层。打开时往里面塞当前图片,背景是黑色半透明遮罩,关闭方式支持两种,点背景关闭,或者按 Esc 关闭。逻辑并不复杂:
const lightbox = document.createElement('div');
lightbox.className = 'lightbox';
lightbox.innerHTML = '<img class="lightbox-image" alt="">';
document.body.appendChild(lightbox);
const lightboxImage = lightbox.querySelector('.lightbox-image');
document.querySelectorAll('.post-content img').forEach((img) => {
img.classList.add('post-image');
img.addEventListener('click', () => {
lightboxImage.src = img.src;
lightboxImage.alt = img.alt || '';
lightbox.classList.add('is-open');
document.body.classList.add('lightbox-open');
});
});
lightbox.addEventListener('click', (event) => {
if (event.target === lightbox) {
lightbox.classList.remove('is-open');
document.body.classList.remove('lightbox-open');
}
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
lightbox.classList.remove('is-open');
document.body.classList.remove('lightbox-open');
}
});
自己写的优势是风格统一,你想让阴影多重、过渡多快、背景多暗,都能和整站保持同一套语言。坏处则是你得自己处理滚动锁定、层级、键盘可访问性这些边角。好在博客图片交互很简单,不是图库站,这套纯 JS 已经够稳了。
移动端 TOC 悬浮球
长文页面的目录在桌面端很好处理,直接固定在右侧边栏就行。屏幕够宽时,目录天然适合一直露出来,用户扫一眼就知道文章结构,也能随时跳转。
移动端不能照搬这一套。手机右侧没空间,再硬塞一个 TOC,只会把正文挤得更难看。我的做法是换成交互更轻的悬浮球,固定在右下角。点一下,目录面板从底部滑出来;再点一次或者点遮罩,就把它收回去。这样目录仍然随时可达,但不占正文位置。
当前阅读位置的高亮,我用的是 IntersectionObserver。页面滚动时观察各级标题,只要某个标题进入视口,就同步点亮目录里的对应项。这里我踩过一个很隐蔽的坑,最初为了和 CSS 保持一致,我把 rootMargin 写成了 rem 单位,结果在移动端表现不稳定。后来才意识到,这里应该老老实实转成 px,再拼进配置字符串里。
const remToPx = (value) => {
const fontSize = parseFloat(
getComputedStyle(document.documentElement).fontSize
);
return value * fontSize;
};
const headerOffset = Math.round(remToPx(5.5));
const observer = new IntersectionObserver(handleHeadingChange, {
root: null,
rootMargin: `-${headerOffset}px 0px -65% 0px`,
threshold: [0, 1],
});
这个修正以后,移动端的目录高亮才真正稳定下来。经验很简单,页面尺寸相关的观察逻辑别想当然,尤其是不同设备、不同根字号一起上场时,能用像素就先用像素。
首页分类卡片
首页除了文章列表,我还加了一组分类卡片作为入口。比起让用户一上来就在时间流里往下翻,先看到分类会更容易建立站点结构感。尤其是博客和 wiki、todo 同时存在时,如果首页只有文章,会显得入口很散。
每张分类卡片都只放两样东西,分类名和文章数量。信息很少,但足够明确。卡片本身延续整站的紫色暗色风格,悬停时边框和阴影略微加强,点击后直接进入对应分类页。这个区域不需要复杂交互,重点是视觉层级干净,让用户一眼知道“这里可以按主题进入”。
从实现角度看,这也是原生 CSS 很舒服的一块。一个简单网格,加上统一的卡片组件,就能把首页组织得很整齐。你不需要为了几个分类入口再接一套额外交互系统,HTML 结构保持轻,后面改样式也很快。
htmx 搜索交互
虽然整个前端坚持原生方案,但搜索框我还是用了 htmx。原因很实际,搜索是典型的“输入一下,拉一块局部结果回来”的场景,用 htmx 正好,不用自己再手写一层请求、状态和 HTML 拼接。
搜索框的写法大概是这样:
<input
type="search"
name="q"
placeholder="搜索文章..."
hx-get="/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#search-panel"
hx-indicator="#search-loading"
autocomplete="off"
/>
<div id="search-panel" class="search-dropdown"></div>
这里最关键的是 hx-trigger="keyup changed delay:300ms"。它让输入框在用户停止输入 300ms 后再发请求,既不会太迟钝,也能避免每敲一个字就打一次接口。返回结果直接塞进下拉面板里,交互上很顺。
我对 htmx 的态度也比较明确,只把它放在搜索和评论这种局部增强场景,不拿它去接管整站行为。这样分工很清楚,页面主体还是普通 HTML,主要样式在 CSS,主要交互在原生 JS,htmx 只处理那种“拉一小块片段回来更新”的需求。好处是心智负担低,不会越写越像半套 SPA。
小结
这一版前端的核心,其实就一句话,少依赖,把体验做细。整站只用了原生 CSS、原生 JavaScript,再加少量 htmx,没有引入大框架,也没有把前端做成复杂应用。对个人博客来说,这样的方案已经足够,而且性能通常也是最好的。
当然,原生路线不是没有代价。你会自己写掉那 1100+ 行 CSS,自己维护那 570+ 行 JS,很多组件规范和交互边角都得亲手管。可换来的回报也很直接,页面轻、控制力强、出了问题好查,而且风格能一直保持统一。
如果你做的是一个以阅读为主、交互不算复杂的个人博客,我很建议你先从这种方案开始。先把颜色、排版、卡片、代码块、图片和目录这些基础体验打磨好,网站自然就会有自己的气质。下一步,再回到功能层面继续扩展就行。
Comments (0)
No comments yet. Be the first!