一、为什么不用现成框架
面试季需要一个个人网站,要求很明确:内容以项目和博客为主、长期可维护、部署到一台只有 nginx 的小服务器(1 核 1G 内存)上,而且不给服务器装任何东西。
流行的方案各有代价:Hexo / Hugo 需要引入构建链和主题,Next.js / Nuxt 需要 Node 运行时常驻,VuePress / VitePress 最终也是静态产物但依赖树很深。对于一个嵌入式工程师来说,最熟悉的工具恰好就是最好的工具——我用 Node.js 内置模块写了一个零依赖静态站点生成器,全部代码不到一千行,构建一秒完成,产物是纯 HTML/CSS/JS。
二、整体架构
src/
├── data/ 站点数据(JSON)
│ ├── site.json 名称、联系方式、导航
│ ├── projects.json 项目数据
│ ├── skills.json 技能分组
│ └── timeline.json 时间线
├── content/posts/ 博客文章(Markdown + front matter)
├── assets/ CSS / JS / 图片
└── lib/md.mjs Markdown 渲染器
build.mjs 构建入口:读数据 → 渲染页面 → 输出 dist/数据与模板分离是核心决策:项目、技能、时间线全部是 JSON,博客全部是 Markdown 文件,加一篇博客只需新增一个 .md 文件然后重新构建,不碰任何代码。
三、几个关键实现
Markdown 渲染器
不引 marked / markdown-it,自己写一个两百行的渲染器。安全策略是先整体 HTML 转义,再处理标记,从根上杜绝用户内容注入:
js
(\w)\n([\s\S]?)```/g, (m, lang, code) => { // 代码块内容此时已转义,直接包 pre 即可 });
let text = escapeHtml(src.replace(/\r\n/g, '\n'));
text = text.replace(/
支持标题(自动生成锚点 id)、表格、代码块(带语言标注)、列表、引用等博客刚需语法。行内代码用占位符保护,避免内部标记被二次处理。
### 零动态内存的工程习惯迁移
写固件时养成的习惯不知不觉带到了网站:容量全部编译期定长、解析器是逐行状态机而不是递归下降、没有正则灾难性回溯。一个手写状态机解析 Markdown,比引入一个 200KB 的库更让人安心。
### 明暗主题与防闪烁
主题偏好存 localStorage,但**读取必须内联在 `<head>` 的同步脚本里**——如果等外部 JS 加载再设置 `data-theme`,深色模式用户会先看到一帧白屏。CSS 用自定义属性组织两套令牌:
### SEO 与订阅
构建时一并生成 `sitemap.xml`、`robots.txt`、`feed.xml`(RSS),每页带 Open Graph 与 JSON-LD 结构化数据(Person / BlogPosting / CreativeWork)。博客页内嵌一份 JSON 数据,前端实现无请求的标题/摘要/标签搜索。
## 四、部署:一台只装了 nginx 的服务器
构建产物 `dist/` 是完全静态的文件,部署只需两步:scp 到服务器 `/var/www/personal-site/`,写一段 nginx 配置。没有 Node 运行时、没有 PM2、没有数据库:
try_files $uri $uri/ $uri.html 一行同时支持目录式 URL(/projects/xxx/)和静态回退,404 也有自定义页面。
五、小结
这个网站本身就是一个嵌入式工程师的作品:零依赖、零运行时、构建一秒、部署一条命令。就像好的固件——上线之后你几乎感觉不到它的存在,但它一直在稳定运行。
后续计划:接入 Gitee 仓库自动同步 README、给博客加目录导航(TOC)。如果也对静态站点生成感兴趣,欢迎邮件交流。