Blumid:开发一款为纯粹阅读而生的 Ghost 主题
酝酿许久,我终于完成了这款一直想开发的博客主题——Blumid,也就是你现在所看到的模样。它基本完整实现了 Ghost 官方提供的所有功能(如搜索、会员、内容卡、原生评论等),更在设计理念和功能细节上倾注了大量心血。
Blumid 的核心是“内容优先”。在这个信息过载的时代,许多博客追求海报级的视觉冲击力,但这往往会分散读者的注意力。Blumid 选择克制与优雅,旨在通过柔和的视觉引导,为读者创造一个沉浸、愉悦的阅读环境。
如果你喜欢这款主题,欢迎你:
-
前往 GitHub 下载并安装到自己的博客上:code-gal/Ghost-Theme-Blumid
-
点亮一颗星 (Star)✨ 来支持这个项目。
-
订阅我的博客,以充分体验它的所有特点,重点在精心设计的侧边栏。
设计理念
我将 Blumid 的核心风格概括为 “晶莫渐变”,旨在突出内容本身,并提供舒适的视觉体验。
-
模块化与现代感:内容以清晰的圆角卡片形式组织,信息层级分明。整体界面简洁,减少视觉干扰,让核心内容成为焦点。
-
柔和氛围:页面背景采用主题色的柔和渐变,导航栏、主体栏及侧边栏等容器则应用了毛玻璃效果。白天模式下呈现淡雅阴影,夜间模式下则带有细微的边框泛光,营造出轻盈、舒适的视觉层次。
-
色彩体系:主色调参考了流行的莫兰迪色系,柔和而不张扬。背景色除了推荐方案外,还完全支持自定义。同时,主题支持根据操作系统偏好自动切换亮/暗模式,并提供手动切换选项。
-
交互贴合习惯:布局全面响应式设计,无缝适配各种屏幕尺寸。导航栏、侧边栏、卡片等所有关键元素都经过精心设计,确保在任何设备上都有一致且流畅的交互体验。
-
轻量与原生:坚持使用原生的 CSS 与 JavaScript,最大限度地减少对第三方框架的依赖,确保主题轻盈、高效。
-
图标系统:使用 Handlebars Partials 嵌入内联 SVG 图标,风格统一,自适应尺寸和主题颜色。
技术规范
作为一个 Web 项目,Blumid 遵循了业界领先的开发标准,确保其稳定、高效且易于维护。
-
100% 通过 Gscan:项目已通过 Ghost 官方的
gscan
工具全面检测,符合主题最佳实践。 -
代码质量:代码结构清晰,注释完善,遵循 BEM 命名规范,并使用现代 CSS 技术 (Flexbox, Grid),
package.json
文件也包含了完整的主题元数据。 -
无障碍访问 (Accessibility):遵循 WCAG 标准,采用语义化 HTML 标签,为交互元素提供
aria-label
,保证足够的色彩对比度并支持键盘导航,致力于提供无障碍的访问体验。 -
SEO 优化:内置了完善的 SEO 结构化数据,有助于提升网站在搜索引擎中的可见性。
-
标准模板结构:主题遵循标准的 Ghost 模板结构,方便开发维护。关键文件及目录包括:
-
default.hbs
: 基础布局模板 -
index.hbs
: 首页文章列表 -
post.hbs
: 文章页模板 -
page.hbs
: 独立页面模板 -
partials/
: 可复用的模板片段 -
locales/
: 语言文件目录 -
assets/
: 存放 CSS、JavaScript、字体和图片等静态资源。
-
核心功能
Blumid 为内容显示实现多种增强效果,并提供丰富的自定义选项,它可以适应个性化需求。
增强的内容体验
-
图片点击放大:文章页和侧边栏中的图片均支持点击放大,方便查看原图细节。
-
便利贴式信息卡:文章的标签、日期等元信息会以别致的便利贴样式展示,并集成了一键分享功能。
-
代码高亮:使用
Prism.js
实现代码高亮,支持显示语言类型和一键复制,并能自适应日夜模式。 -
Mermaid 图表:原生支持
Mermaid
语法,只需在 Markdown 或 HTML 中标注语言类型,即可将代码块渲染为流程图、时序图等。 -
LaTeX 公式:通过
KaTeX
解析,支持在文章页中渲染数学公式。为获得最佳效果,建议将公式写入math
或latex
类型的代码块中。 -
优化的内容卡:表格、引用以及所有 Ghost 官方内容卡都经过设计,确保在各种设备上都有美观且响应式的显示效果。
丰富的自定义选项
你可以在 Ghost 后台的 Design & branding 中轻松完成所有设置,或通过 Code injection 实现更高级的定制。
1. 全屏首页
可选的沉浸式首页,可用于展示精美的背景图与网站元信息,适合需要内容预加载或希望第一时间展示关键信息的网站。用户可通过滚动或点击箭头平滑过渡到文章列表。
2. 导航
头部和页脚导航栏分别展示Ghost 后台设置的主要和次要的导航条目,头部导航栏滚动自动显隐,小屏幕下自动折叠,小图标按钮切换侧边栏显示、颜色模式、搜索和会员。
3. 侧边栏收纳
Page页是没有侧边栏的,你可以选择完全关闭,打造极简的阅读体验。如果打开,它会智能响应不同设备,交互方式清晰自然:
-
手机/平板:从屏幕左侧向右滑动可呼出侧边栏,点击内容区域外即可隐藏,避免与阅读手势冲突。
-
桌面端:通过点击导航栏的图标来显示或隐藏。
4. 多功能侧边栏内容
在文章页,侧边栏会自动生成文章大纲。在列表页(首页、标签页等),你可以选择展示三种不同内容:
- 网站信息 (Site_Info):默认模式,展示网站 Logo、作者信息和标签云。
- RSS Feed:订阅并展示外部 RSS 源,非常适合聚合你在其他平台(如 Memos、Blinko、Mastodon)发布的内容。
-
代码注入 (Code_Injection):通过注入自定义脚本,嵌入聊天框、个人状态等独特模块。
-
会员专属内容:你可以将 RSS Feed 或代码注入的侧边栏内容设置为“仅会员可见”,为订阅用户提供专属价值。
5. 自定义渐变背景
背景颜色支持完全自定义,你可以随心所欲地调整日间与夜间模式下的顶部和底部色彩。
推荐颜色配置:
-
日间:
#A0C4FF
(天空蓝),#FFFFE0
(阳光黄),#E4F9E0
(青草绿),#FFFFFF
(纯白) -
夜间:
#000000
(星空黑),#2C003E
(霓虹紫),#C76D00
(落日橙),#7D7D7D
(纯灰)
6. 自定义字体与笔头
主题内置了优雅的字体回退方案,并允许你通过后台或代码注入引入自定义字体(如霞鹜文楷)。
文章分隔线上的“笔头”图标也支持替换(支持 SVG 和 PNG),增添个性化细节。
7. 多语言支持
主题内置中、英双语,并会根据 Ghost 后台的语言设置自动切换。你也可以轻松在 locales
文件夹中添加更多语言的翻译文件。
进阶功能详解
关于侧边栏 RSS 的 CORS 问题
由于浏览器同源策略,直接在前端请求第三方 RSS 源可能会遇到 CORS 限制。解决方案如下:
-
控制自己的源:如果 RSS 源站属于你,可以在服务器响应中添加
Access-Control-Allow-Origin
等头部。 -
使用 CORS 代理:对于无法控制的第三方源,可以使用 CORS 代理服务。其原理是让代理服务器去请求 RSS 源,然后将结果返回给你的博客,从而绕过浏览器限制。
一些公共 CORS 代理服务对比:
代理名称 | URL | 工作方式 | API 密钥 | 速率限制/备注 |
---|---|---|---|---|
allorigins.win | https://api.allorigins.win |
/get?url={TARGET_URL} |
不需要 | 免费开源,速率限制未明确说明。 |
CORS.SH | https://proxy.cors.sh |
/{TARGET_URL} |
需要 | 提供免费(限开源项目)和付费套餐。 |
cors-anywhere | https://cors-anywhere.herokuapp.com |
/{TARGET_URL} |
不需要 | 仅为演示服务器,不稳定,不建议生产使用。 |
Cloudflare Worker | https://test.cors.workers.dev |
/?url={TARGET_URL} (示例,具体取决于worker实现) |
不需要 | Cloudflare Worker的演示实例,用于测试。可以自建。 |
使用示例:若你的 RSS 源是 https://example.com/rss
,你可以在后台设置为 https://api.allorigins.win/get?url=https://example.com/rss
。
关于侧边栏代码注入
此功能为你提供了一个锚点元素 <div id="initChatBox" class="sidebar-section"></div>
,你可以通过它在侧边栏嵌入任何动态内容,例如微博时间线或一个独立的聊天盒子。
我特别推荐结合 Cactus Comments 使用。如果你部署了自己的 Matrix 服务,它可以作为一个功能强大的网站聊天室,甚至是一个独立的短内容发布渠道,与本主题的会员系统和样式完美融合。参考教程:Matrix 评论系统:我为 Cactus Comments 开发了些新功能。
在这个主题上,有一些特别的推荐设置:
<link rel="stylesheet" href="https://.../cactus-style.css" type="text/css">
<script type="text/javascript" src="https://.../cactus.js"></script>
<script type="text/javascript" src="https://.../cactus-lang.js"></script>
<script>
document.addEventListener('DOMContentLoaded', async function() {
// 1. 定义获取Ghost会员名的函数
async function getGhostMemberName() {
try {
// 尝试访问Ghost会员API端点
const response = await fetch('/members/api/member/');
if (response.status === 204) {
// 204 No Content - 表示会员未登录
return null;
}
if (response.ok) {
const member = await response.json();
if (member && member.name && member.name.trim() !== '') {
// console.log('Ghost Member: Name found -', member.name);
return member.name;
} else if (member && member.email) {
// 如果没有名字,可以考虑使用邮箱前缀作为备选
const emailUsername = member.email.split('@')[0];
return emailUsername;
}
console.log('Ghost Member: Logged in, but no usable name or email found.');
return null;
} else {
console.error('Ghost Member: Error fetching member data -', response.status);
return null;
}
} catch (error) {
console.error('Ghost Member: Exception fetching member data -', error);
return null;
}
}
// 2. 尝试获取Ghost会员名
const ghostUserName = await getGhostMemberName();
// 3. 初始化聊天盒子 (Cactus Comments)
const chatBoxContainer = document.getElementById("initChatBox");
if (chatBoxContainer) {
// 确保 initComments 函数已在全局作用域定义或通过其他方式引入
if (typeof initComments === 'function') {
initComments({
node: document.getElementById("initChatBox"),
defaultHomeserverUrl: "https://matrix.xxx.com:8448",
serverName: "xxx.com",
siteName: "xxx",
loginEnabled: true,
guestPostingEnabled: true,
commentEnabled: true,
updateInterval: 60,
pageSize:10,
commentSectionId: "blog",
isAuthenticated: true,
translationsData: cactusTranslations,
language: "cn"
});
// 4. 如果获取到了Ghost会员名,则尝试填充到聊天输入框
if (ghostUserName) {
let attempts = 0;
const maxAttempts = 15; // 尝试次数 (15 * 500ms = 7.5秒)
const interval = 500; // 尝试间隔 (毫秒)
function attemptToFillUsername() {
// 根据您提供的HTML结构定位输入框
const nameInput = document.querySelector(".cactus-editor-name input[type='text']");
if (nameInput) {
nameInput.value = ghostUserName;
// 触发 'input' 事件,以便聊天客户端可能依赖的事件监听器能够感知到值的变化
nameInput.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
// 有些组件可能监听 'change' 事件
nameInput.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
} else if (attempts < maxAttempts) {
attempts++;
setTimeout(attemptToFillUsername, interval);
} else {
console.warn('ChatBox: Username input field not found after multiple attempts. Could not prefill with Ghost member name.');
}
}
attemptToFillUsername(); // 开始尝试填充
}
} else {
console.error('Error: initComments function is not defined. Cannot initialize chatbox.');
}
}
});
</script>
<style>
.cactus-container {
flex-direction: column-reverse;
}
.cactus-comments-container {
flex-direction: column-reverse;
}
.cactus-comments-list {
flex-direction: column-reverse;
}
.cactus-comment-avatar{
width: 25px !important;
}
.cactus-comment-avatar-placeholder,
.cactus-comment-avatar>img {
width: 25px !important;
height: 25px !important;
}
.cactus-login-form-wrapper {
position: sticky;
}
.cactus-editor-name {
display:none;
}
.cactus-comment{
gap: 0.5em;
}
</style>
评论