演示地址:https://blog.mickeyai.top/repo/
一、构思:为什么博客需要一个"仓库"
做独立开发的人手里总攒着几个项目:文档引擎、图像工具、网址导航、游戏圈、图标库、社区、微博……它们散落在各个平台,访客来了博客,却看不到这些"作品"。于是萌生了给博客加一个源码仓库模块的想法,不是简单的一个链接列表,而是一个有 GitHub 气质的作品陈列馆:
- 每个项目是一张"仓库卡":名称、简介、语言、协议、Star/Fork/浏览量
- 按分类浏览,有热门推荐,能按最新/最热/最多星排序
- 访客可以登录后点星,作者拥有自己的个人仓库空间页
- 项目带 README 说明文档,支持截图粘贴上传

技术栈选了最熟悉的 PHP + MySQL,不引入框架,静态博客的轻盈感不能丢。
二、设计:先像 GitHub,再是自己的
视觉上的核心决策是全面 GitHub 化,因为"仓库感"是一种已经被训练出来的视觉直觉:
- 等宽字体的 owner / repo-name 路径式标题:一眼"这是仓库";
- Star/Fork 做成右上角按钮组而不是报表数字,星星点亮后变金色;
- README 装进带标题栏的线框容器,像一份"打开着的文档";
- 语言色点沿用 GitHub 官方配色(PHP 紫、JS 黄、Python 蓝),未收录的语言用主题色兜底。
布局上做成左中右三栏:左侧是吸顶的分类按钮(每个带实时数量徽章),中间是仓库列表,右侧是"推荐源码 + 关于 + 热门仓库"。整个模块黑白灰为主,唯一的彩色是语言点和分类标签。
图标系统也统一了:全站废弃 emoji,改用 GitHub Octicons 的 SVG 路径,通过 CSS mask 渲染——颜色跟随 currentColor,白天黑夜自动适配,不需要写任何额外的暗色规则:
.ra-ico {
display: inline-block;
width: 14px; height: 14px;
background-color: currentColor; /* 颜色 = 文字颜色,黑白模式自动跟随 */
mask: url("data:image/svg+xml,...star路径...") center / contain no-repeat;
}

三、落地:四个值得说的细节
用户提交的 README,怎么防 XSS?
详情页需要渲染用户填写的说明文档。第一版方案是"存 HTML + 服务端白名单消毒",但很快发现更优雅的解法:存 Markdown 原文,渲染时先整体转义再解析,用户贴的任何 HTML 都会变成纯文本显示,XSS 从根上不可能,连消毒函数都不需要。写了一个 60 行的迷你渲染器,支持标题、加粗、代码块、列表、引用、链接和图片,配上一个极简工具栏,按钮直接插入 Markdown 标记。
截图即贴即传。
写文档最烦的就是传图。编辑器监听了 paste 事件,剪贴板里有图片就直接上传——截图、PS 选区、微信复制的图,Ctrl+V 一下就插进正文。上传接口做了登录校验、MIME 白名单(不信前端传的类型)、5MB 上限、随机文件名,图片落在独立的 /uploads/repo/ 目录。

固定边栏的翻车与自救。
"左右边栏固定、中间滚动"这个需求,position: sticky 在本地好好的,上线就失灵——主题的全局样式里有 overflow 祖先,sticky 会静默失效。换成 position: fixed 又引入新问题:100vw 包含滚动条宽度,边栏一切过去就横向溢出,页面"跳一下";滚到底部还会盖页脚。最终方案是transform 跟随:边栏留在文档流里不动,JS 用 translateY(滚动距离) 模拟固定,接近页脚时钳制位移上限——布局零变化、不溢出、不盖页脚,一次到位。
"更新于 2 分钟前"的灵异事件。
详情页刚上线就有访客反馈:项目从没编辑过,却显示"更新于 2 分钟前"。查下来是 MySQL 的经典坑:updated_at 字段带 ON UPDATE CURRENT_TIMESTAMP,而详情页每次访问都会执行 views + 1,每被看一次,"更新时间"就被顶成现在。解法是一行 ALTER TABLE 去掉自动刷新属性,编辑页保存时本来就显式写 updated_at = NOW(),真实编辑仍能正确更新。

四、结果与思考
模块上线后,列表页的统计带、排序切换、热门徽章(获星 ≥ 3)让站点第一次有了"社区感"。回头看,这个模块没有高深技术,全是产品判断 + 工程细节:GitHub 化是为了降低认知成本,Markdown 是为了降低写作门槛,粘贴上传是为了消灭操作摩擦。
最大的体会是:用户不会告诉你"我想要 sticky 定位",他们只会说"边栏没固定住"。把抱怨翻译成机制,再为机制挑选最稳的实现——这比任何框架都重要。