FIT2CLOUD
X 的图标

X

1,068
155

高仿 X(Twitter)的 Halo 2.x 主题。三栏时间线布局、Lights out / Dim / Light 三档配色、推文形态的文章卡片、认证蓝标、作者主页与站内发帖 —— 既可以把博客当作一个 X 账号来逛,也能在开放注册后当作一个小型内容平台来运营。

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 里,改名时特意没有跟着换,所以老用户覆盖安装后配置照旧。

  1. 把 theme-x 目录打包为 zip(要求 zip 内包含 theme-x/ 顶层目录)
  2. 进入 Halo 后台 → 外观 → 主题 → 安装主题 → 上传该 zip
  3. 启用主题后,进入 主题设置 调整配色与开关

要求 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-TOKEN cookie 是 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。

作者主页封面图

主题按下面的顺序取封面:

  1. 该作者 User 资源上的 theme-x/cover 注解(给单个作者定制,插件可写)
  2. 主题设置 → 账号信息 → 作者主页默认封面图
  3. 再退回上面的「独立页面封面图」
  4. 都没有就用主题自带的渐变背景

目前 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. 无任何关联。


讨论(0)

这是一个开源应用,我们推荐优先在开源仓库中提交 issue,以便开发者更好地接收和跟进问题。

去提交 issue