← 与Obsidian同步的博客搭建

与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!