X — 高仿 X(Twitter)的 Halo 2.x 主题
把博客当作一个 X 账号来逛:首页是时间线,文章是推文,标签是话题,作者是主页。
打开注册、让更多人进来发内容,它也能当一个小型内容平台来用。
关于本项目
本主题的绝大部分代码由 WorkBuddy(模型 DeepSeek V4.1 Flash)生成,人工负责提出需求、审阅与验收。
从「页面布局契约缺失」到「后台设了暗色却总是亮色」,中间踩过的坑都记在提交历史里,可以当排错参考。
欢迎提 Issue。
特性
布局
- 桌面端三栏:左导航 275px / 主栏 600px / 右栏 350px(宽屏 ≥1300px 时 400px),整体居中于 1265px 容器
- 移动端单栏 + 底部五宫格导航 + 悬浮按钮
- 断点:
≤1265px左栏收窄为纯图标;≤1005px隐藏右栏;≤705px单栏 + 底部导航
配色
- 三档方案,与 X 完全一致:
Lights out(纯黑,默认)/Dim(深蓝灰#15202B)/Light - 支持「跟随系统」,右上角可随时切换,选择写入
localStorage - 切换菜单里有一项 「跟随站点设置」,用于清除本地覆盖、回到你在主题设置里配的方案
- 本地选择带来源标记:只有「本地选择」与「当前站点默认值」是同一次设置产生的才生效;站点设置一改,旧的本地覆盖自动作废。这样不会出现「后台设了暗色、页面却一直是亮色」
- 强调色、三档背景色、正文字号、圆角、密度均可在主题设置中调整
html上始终写「解析后的具体值」(dark/dim/light),原始选择另存于data-preference,避免system这种 CSS 匹配不到的值导致样式失效- CSS 内置
html:not([data-theme])的兜底变量,即使 JS 未执行也不会回退成浏览器默认白底
内容呈现
- 推文卡片:头像 · 显示名 · 认证标 ·
@用户名· 相对时间,正文 + 封面图 +#话题,底部互动栏(回复 / 复制链接 / 点赞 / 浏览 / 收藏 / 分享) - 互动图标沿用 X 的 hover 语义:回复变蓝、转发变绿、点赞变粉并带弹跳动效
- 相对时间对齐 X 的呈现:
现在/5分钟/3小时/8月14日/2024年8月 - 首页双 Tab:
为你推荐(全部)与精选(置顶文章),纯前端切换 - 文章详情页还原推文详情:23px 标题、17px 正文、发布时间与浏览量、回复区
页面覆盖
| 模板 | 页面 | X 的对应形态 |
|---|---|---|
index.html |
首页 | Home 时间线 |
post.html |
文章详情 | Post 详情 + Replies |
post_longform.html |
长推文(自定义模板) | 长文排版,19px 正文 |
page.html |
独立页面 | 单一内容页 |
page_profile.html |
个人主页(自定义模板) | Profile 封面 + 账号区 |
archives.html |
归档 | 按年月的完整时间线 |
tags.html / tag.html |
话题集合 / 话题归档 | Explore 网格 / #话题 流 |
categories.html / category.html |
分类集合 / 分类归档 | Explore 网格 / 分类流 |
author.html |
作者归档 | Profile 主页 |
moments.html |
瞬间列表(可选) | Home 时间线 |
moment.html |
瞬间详情(可选) | Post 详情 + Replies |
search.html |
搜索 | 全文检索结果流 |
error/404.html、error/error.html |
错误页 | — |
能力
- 全文搜索:调用 Halo 内置索引接口
POST /apis/api.halo.run/v1alpha1/indices/-/search - 点赞:走
POST /apis/api.halo.run/v1alpha1/trackers/upvote,用localStorage记录已赞状态避免重复提交 - 评论:通过
<halo:comment>扩展点接入评论插件 - 页脚:保留
<halo:footer />,兼容系统级代码注入
安装
主题标识:
halo-theme-x。早期版本叫theme-x,上架应用市场时该名称已被占用,故改名。
主题设置存在theme-x-setting这个 ConfigMap 里,改名时特意没有跟着换,所以老用户覆盖安装后配置照旧。
- 把
theme-x目录打包为 zip(要求 zip 内包含theme-x/顶层目录) - 进入 Halo 后台 → 外观 → 主题 → 安装主题 → 上传该 zip
- 启用主题后,进入 主题设置 调整配色与开关
要求 Halo
>= 2.26.0—— 主题依赖 2.26 引入的页面布局契约(templates/layout.html)与用户中心 API。
开发调试时建议开启thymeleaf.cache: false(或环境变量SPRING_THYMELEAF_CACHE=false)。
功能性还原 X(可选,依赖社区插件)
外观之外,主题对 X 的核心功能也做了映射。以下插件均为可选,装了才有对应能力,不装不影响主题运行。
| X 的能力 | 实现方式 | 依赖 |
|---|---|---|
| 时间线(推文流) | moments.html — 纯内容流,无标题,支持图文/视频/音频 |
瞬间插件 halo-plugin-moments |
| 推文详情 | moment.html — 大头像、17px 正文、媒体、点赞/回复数、回复区 |
同上 |
| 回复 | <halo:comment> 接入评论插件 |
评论插件(需支持 moment.halo.run / Moment 主体) |
| 点赞 | POST /apis/api.halo.run/v1alpha1/trackers/upvote,请求体 {group, plural, name}。文章用 content.halo.run/posts,瞬间用 moment.halo.run/moments |
无 |
| 话题 / 趋势 | 标签体系 + 右栏「正在发生」 | 无 |
| 搜索 | 全文索引 + 瞬间内容也进索引 | 无 |
| 订阅 | RSS(含 /moments/rss.xml) |
无 |
| 收藏 | 本地 localStorage | 无 |
安装瞬间插件后,导航会自动多出「瞬间」一项(可在主题设置里关闭)。瞬间列表支持按标签筛选:/moments?tag=xxx。
关于评论:瞬间的回复依赖评论插件支持
moment.halo.run / Moment这个评论主体。若回复区不显示,可在主题设置里调整「瞬间评论 · 模型分组 / 类型」两个字段,按你的评论插件实际值填写。
读者注意:主题内所有路由(分类 / 标签 / 归档)都读取
site.routes.*,不硬编码。若你在后台改过路由前缀(例如归档页改成/posts),导航会自动跟随。
登录与账号
主题内置了完整的登录态处理,全部基于 Halo 原生的 Spring Security 方言,无需额外插件:
| 位置 | 未登录 | 已登录 |
|---|---|---|
| 顶栏右上角 | 「登录」「注册」按钮 | 登录用户头像 + 昵称,点击进作者主页 |
| 左栏底部 | 「登录」「注册」按钮(上下排列) | 登录用户卡片(头像 / 昵称 / @用户名),点击进作者主页 |
| 右栏顶部 | 站点所有者卡片(取主题设置里的账号信息) | 登录用户卡片,名字进作者主页,「个人中心」按钮保留旧 /uc 入口 |
不再事事都跳 Halo 个人中心
主题在 author.html / layout.html / index.html 里内建了两套原本要跳 /uc 的能力:
原本要跳 /uc/moments |
现在 |
|---|---|
| 首页顶部「有什么新鲜事?」、移动端右下角蓝色按钮 | 打开主题内的 X 风格发帖弹层(遮罩 + 大头像 + 无边框输入区 + 字数统计 + 「所有人可见」提示),直接调 POST /apis/uc.api.moment.halo.run/v1alpha1/moments 发布,成功后原地刷新 |
原本要跳 /uc「个人资料」 |
现在 |
|---|---|
作者本人访问自己的作者主页(/authors/{你的用户名}) |
看到 「编辑个人资料」 按钮,弹层里可改名称 / 简介 / 头像,头像支持本地预览,存完自动刷新 |
旧入口没有删:右栏账号卡片的「个人中心」按钮、以及作者主页资料区里为作者本人准备的「个人中心(旧入口)」链接,都仍然指向 /uc(地址可在主题设置里改)。
这两处能力都依赖 Halo 的用户中心 API(
/apis/uc.api.*下的UcUserEndpoint,自 2.26.0 起提供),
这也是主题把requires定在>= 2.26.0的原因。为什么主题里能直接写数据:Halo 的 CSRF 只保护表单类端点,
CsrfConfigurer里用NegatedServerWebExchangeMatcher明确把/api/**、/apis/**、/actuator/**、/system/setup、/sudo/**排除在保护之外,所以带会话 cookie 直接fetch即可,不需要任何令牌(那个XSRF-TOKENcookie 是 HttpOnly 的,前端本来也读不到)。
登录 / 注册页面已适配 X 风格
主题覆盖了 templates/gateway_fragments/layout.html,采用官方推荐的**「自定义样式」**方式:
- 只引入 Halo 的逻辑脚本
gateway_fragments/common::basicScriptResources(密码 RSA 加密、表单提交、验证码等全部由 Halo 提供) - 样式完全由主题接管,通过
templates/assets/css/x-gateway.css把登录 / 注册 / 找回密码等页面渲染成 X 风格 - 不改动任何表单结构,因此不存在登录流程被破坏的风险
片段签名严格遵循契约:layout (title, head, body),其中 body 由 Halo 注入具体的认证表单。
其它实现要点
- 登录态判断用
sec:authorize="isAnonymous()"/isAuthenticated() - 当前用户用
${contributorFinder.getContributor(#authentication.name)}取得(返回 ContributorVo,含displayName/avatar/name/bio) - 注册入口会同时检查
site.allowRegistration,系统设置里没开「允许注册」就不会显示 - 作者主页(
/authors/{name})会展示该用户的加入时间(author.spec.registeredAt)
页面布局契约(Halo 2.26.0+)
主题在 templates/layout.html 中声明了 html(head, content) 片段,符合 Halo 的 v1 页面布局契约。插件提供的前台页面(瞬间、链接、图库等)会自动套用本主题的左导航、右栏、配色与响应式外壳,无需额外适配。
head:调用方提供的头部片段;主题只在head为空时输出站点标题,避免同一页面出现两个<title>content:调用方提供的正文片段,插入到主栏中- 页面底部保留
<halo:footer />,Halo 与插件仍可注入内容
主题自身的 14 个页面复用同一份布局(~{layout :: html(head = ~{::head}, content = ~{::content})}),不存在第二套外壳代码。在 Halo 后台的主题详情里,Theme.status.pageLayout 应显示为 SUPPORTED。
因为契约只允许
head/content两个参数,「当前栏目高亮」在客户端按location.pathname判定;同时在客户端判定的还有相对时间格式化与阅读时长。
目录结构
theme-x/
├── theme.yaml # 主题元信息
├── settings.yaml # 主题设置表单
├── logo.svg # 主题图标源文件
└── templates/
├── layout.html # 页面布局契约(html(head, content))+ 全局骨架
├── index.html # 首页时间线
├── post.html # 文章详情
├── post_longform.html # 自定义模板:长推文
├── page.html # 独立页面
├── page_profile.html # 自定义模板:个人主页
├── archives.html # 归档
├── tags.html / tag.html # 话题集合 / 归档
├── categories.html / category.html
├── author.html # 作者主页(含「编辑个人资料」)
├── search.html # 搜索
├── moments.html # 瞬间时间线(瞬间插件)
├── moment.html # 瞬间详情
├── error/
│ ├── 404.html
│ └── error.html
├── gateway_fragments/
│ └── layout.html # 认证页契约 layout(title, head, body)
├── modules/
│ ├── left-nav.html # 左侧导航
│ ├── right-rail.html # 右侧栏(搜索 / 账号卡片 / 正在发生)
│ ├── mobile-nav.html # 移动端底部导航
│ ├── account.html # 顶栏账号区
│ ├── account-menu.html # 左下角账号卡片「…」弹出的菜单
│ ├── post-card.html # 推文卡片组件(文章)
│ ├── moment-card.html # 推文卡片组件(瞬间)
│ ├── verified.html # 认证蓝标(带用户名白名单)
│ ├── compose.html # 站内发瞬间弹层
│ └── profile-edit.html # 个人资料编辑弹层
└── assets/
├── css/x.css # 全部样式
├── css/x-gateway.css # 认证页样式
└── js/main.js # 交互脚本
主题设置
| 分组 | 设置项 | 说明 |
|---|---|---|
| 外观配色 | 默认配色方案 | dark / dim / light / system。这是全站唯一权威来源,你设成什么访客就看到什么 |
| 允许访客自行切换配色 | 默认关闭。关闭时右上角不出现切换按钮,访客不可能把配色弄偏;打开后才出现按钮,且访客的选择只记在它自己浏览器里,你一改上面的方案就自动作废 | |
| 强调色 | 默认 #1D9BF0,可整体换色 |
|
| 背景色(三档) | Lights out / Dim / Light 的背景分别可改,默认 #000000 / #15202B / #FFFFFF |
|
| 正文字号 | 14 / 15 / 17 | |
| 媒体大圆角 | 关闭后媒体改为 4px 圆角 | |
| 时间线形态 | 舒适 / 紧凑(紧凑隐藏封面与摘要) | |
| 账号信息 | 用户名 / 显示名 / 简介 | 留空回退到站点标题与副标题 |
| 头像 / 封面图 | 覆盖站点 Logo | |
| 显示认证蓝标 | 总开关,关掉后全站不显示蓝标 | |
| 带认证标识的用户 | 用户名白名单,英文逗号分隔。留空 = 所有作者都显示;填了就只给名单里的用户名显示(用户名即 /authors/ 后面那段) |
|
| 布局 | 左导航 / 右栏 / 发帖框 / 底部导航 | 桌面端或移动端开关 |
| 内容与交互 | 统计数据显示 / 点赞 / 封面 / 评论 / 搜索 / 趋势 | 功能级开关 |
| 作者主页显示加入日期 | 对任何作者的 /authors/{用户名} 都生效。优先取注册时间,缺失时回退到账号创建时间;关闭后整块隐藏 |
|
| 瞬间投稿入口 | 移动端右下角悬浮按钮 + 首页顶部「有什么新鲜事?」。未登录时文案变为「登录后即可发布瞬间」,点击跳登录页并带上回跳地址 | |
| 站内直接发瞬间 | 默认开启。开启后投稿入口打开主题内的 X 风格弹层;关闭则退回跳转下面的「瞬间发布地址」(默认 /uc/moments) |
|
| 作者主页内编辑个人资料 | 默认开启。开启后作者本人在自己的作者主页会看到「编辑个人资料」 | |
| 路由与账号入口 | 登录 / 注册 / 个人中心地址,瞬间发布地址,作者页与瞬间页前缀,登录注册开关 | 改过后台路由就改这里,避免链接 404 |
| 导航自定义 | 主页 / 瞬间 / 探索 / 话题 / 时间线 / 搜索 六项 | 每项可选 常驻显示 / 折叠到「更多」/ 隐藏;底部导航跟随同样的设置 |
| 自定义导航项 | 用 FormKit 的 repeater(卡片式条目),可自由增删、上下排序。每项含 名称 / 链接 / 图标 / 位置;名称或链接留空即不显示。支持内链(/tags)与外链(自动新窗口打开) |
|
| 站点标识 | X Logo / 自定义图片 / 文字 | 文字模式支持副标题;图片模式建议正方形 |
设置值在模板中通过 theme.config.[group].[name] 读取。
关于配色:站点设置说了算
v1.7.1 之前这里有个很容易踩的坑,值得记一笔:
早期版本在右上角放了一个配色切换菜单,里面把 light 标成了「默认」。任何人(包括站长自己)
点一下这个「默认」,就会在自己的浏览器里永久写入一条「覆盖成亮色」的偏好。而初始化脚本
只在「站点默认值变化过」时才作废这条覆盖 —— 如果站长一直用 dark 没动过,这条覆盖就永远赢,
表现就是后台怎么改都强制亮色,换设备/换浏览器却正常(因为新浏览器没有这条脏数据)。
现在的规则简单且不可绕:
- 主题设置里的「默认配色方案」是唯一权威来源
allow_visitor_switch默认关闭:初始化脚本完全不读 localStorage,并顺手清掉历史脏数据- 打开开关后,访客覆盖会带上「站点当前设置」的指纹,站长一改设置它立刻作废
- 切换菜单底部会直接写出「站点设置:熄灯 / 当前生效:跟随站点设置」,状态一眼可见
多作者与平台化
主题最初是按「一个人写博客」设计的,但功能上并没有限死在这上面:
- 作者主页:
/authors/{用户名}对任何作者都生效,展示头像、简介、加入日期和他的全部文章 - 认证蓝标:支持用户名白名单,只给名单里的作者显示
- 站内发帖:任何登录用户都能发瞬间(是否需要审核由 Halo 的角色权限决定)
- 账号菜单:左下角账号卡片右侧的「…」可以弹出菜单,条目在主题设置里自由增删
- 作者主页封面:默认共用一张封面;想给某个作者单独定制,在他的 User 资源上写
theme-x/cover注解即可 —— 社区插件或后台编辑用户时写入都行
账号菜单
主题设置 → 导航自定义 → 账号菜单项,卡片式条目,可以自由增删排序。
| 字段 | 说明 |
|---|---|
| 名称 | 菜单文字,留空则该条不显示 |
| 链接 | @uc 表示 Halo 自带的用户中心(跟随「个人中心地址」设置,改路由时不用改两处);也可以填 /moments 这样的内链,或完整外链(自动新窗口打开) |
| 图标 | 从主题内置图标里挑 |
默认给了一条「用户中心」。想加「我的主页」「我的瞬间」「退出登录」之类,往列表里加就行 ——
退出登录填 Halo 的 /logout。
作者主页封面图
主题按下面的顺序取封面:
- 该作者 User 资源上的
theme-x/cover注解(给单个作者定制,插件可写) - 主题设置 → 账号信息 → 作者主页默认封面图
- 再退回上面的「独立页面封面图」
- 都没有就用主题自带的渐变背景
目前 Halo 生态里没有现成的「用户封面图」插件,所以主题把接口留在了注解上:
任何能写入User.metadata.annotations['theme-x/cover']的插件都能直接驱动它。
也可以直接在后台编辑用户、手工加这条注解。
已知边界
- 搜索依赖 Halo 内置的全文索引,若站点未启用索引会返回空结果
- 点赞用的是
trackers/upvote,只支持点赞、不支持取消(Halo 只有这一个端点,没有对应的 DELETE)。
服务端不做去重,主题用localStorage记录已赞状态;清掉浏览器数据后再点会重复计数,这是 Halo 的既定行为 - 浏览数由主题在详情页上报一次
trackers/counter,用sessionStorage保证同一篇在一次会话里只报一次 - 评论需要另行安装评论插件;未安装时提示区会显示占位文案
- 归档页的年份标题使用了
sticky定位,在部分旧版浏览器上会退化为普通标题 - 站内发瞬间依赖瞬间插件;发布后是否需要审核由 Halo 的角色权限决定(站长 / 有发布权限的账号立即通过,其他账号会进入待审核,这也是插件侧
momentService.create的既定行为) - 编辑个人资料只能改显示名、简介与头像 —— 用户名与邮箱不在
UpdateMyProfileRequest里,属于 Halo 的接口边界,不是主题的取舍
与 halo-plugin-X 联动(可选)
halo-plugin-X 是本主题的配套插件,用来补上 Halo 本身没有的那几项个人资料:
| 字段 | 显示位置 | 存放 |
|---|---|---|
| 个人主页背景图 | 作者主页顶部横幅 | User.metadata.annotations["theme-x/cover"] |
| 位置 | 资料行 | User.metadata.annotations["theme-x/location"] |
| 个人网站 | 资料行(可点击) | User.metadata.annotations["website"] |
主题不依赖这个插件。 装上之后,「编辑个人资料」弹层里会多出这三项;没装则那一块整块隐藏,
主题其余部分完全不受影响 —— 插件只做增强,不成为前提。
探测方式很直接:主题向 GET /apis/x.api.halo.run/v1alpha1/my-profile 发一次请求,
成功就显示扩展字段,失败就保持隐藏。保存时两个接口分开调用,扩展资料写失败也不会拦住显示名和简介。
因为数据存的是 User 的注解,你也可以不装插件、直接在后台给某个用户加上这些注解,主题一样会认。
回复区样式
回复区用的是 Halo 评论插件(halo-plugin-comment-widget)。它是 Lit + Shadow DOM 组件,
内部类名从主题 CSS 里盖不到 —— 但它把所有配色收敛成了 --halo-cw-* 自定义属性,
而自定义属性能穿透 Shadow 边界,这就是官方的定制入口。
主题把整套变量映射到了自己的配色上:
| 插件变量 | 含义 | 映射到 |
|---|---|---|
--halo-cw-primary-1/2/3-color |
链接、焦点环、按钮 | 主题强调色 |
--halo-cw-text-1/2/3-color |
文字三级 | --x-text / --x-dim |
--halo-cw-muted-1/2/3-color |
背景与边框三级 | --x-border / --x-bg-active / --x-card |
--halo-cw-base-font-size / -family |
字号、字体 | 跟主题走 |
这一步不是可选项。插件靠
.dark类或prefers-color-scheme判断深色,
而本主题用的是data-theme属性,插件识别不到 —— 不覆盖的话,
深色主题下评论区会退回浅色配色,文字和背景糊在一起。
想再改,直接覆盖同名变量即可;不要试图去写 .item-avatar 之类的内部类名,那些在 Shadow DOM 里。
动效
v1.8.0 把散落各处的硬编码缓动收敛成一套变量(--x-ease / --x-ease-out / --x-dur-fast / --x-dur / --x-dur-slow),改一处全站生效,并补齐了此前缺失的过渡:
| 场景 | 效果 |
|---|---|
| 时间线 / 回复区 / 右栏卡片 / 详情正文入场 | 轻微上浮淡入,前 8 项按 20ms 错峰 |
| 主题菜单、「更多」折叠菜单 | 缩放 + 淡入 |
| 弹层遮罩 | 淡入 |
| 移动端弹层 | 从底部滑入 |
| 按钮 / 图标按钮 / 导航项 / 底部导航 / 悬浮按钮 | 按压缩放反馈 |
| 左导航图标 | hover 放大 8% |
| 探索网格卡片 | hover 上浮 2px |
| 推文封面图 | hover 缓慢放大 2% |
| 图片载入 | 淡入(缓存命中的直接跳过,不会二次闪烁) |
顺手修了一个隐藏 bug:.x-post { display: flex } 会盖掉浏览器默认的 [hidden] { display: none },
导致首页「精选」标签页实际切不动。现在补了全局 [hidden] { display: none !important }。
所有动效在 prefers-reduced-motion: reduce 下会被完全关闭(不只是把时长压到 0.01ms,而是取消动画本身)。
从源码安装
本仓库的根目录就是主题本体。Halo 要求 zip 内包含一个顶层 theme-x/ 目录,所以克隆后:
git clone https://github.com/<你的用户名>/halo-theme-X.git theme-x
zip -r theme-x.zip theme-x -x 'theme-x/.git*'
然后到 Halo 后台 → 外观 → 主题 → 安装主题 上传 theme-x.zip。
授权
MIT License。
主题为界面风格的致敬实现,X 的名称与标识归 X Corp. 所有,本项目与 X Corp. 无任何关联。










