用 Node.js 搭一个够用的静态站点
内容型站点不一定需要前端框架。一个小的构建脚本、一套模板和几条部署校验,已经足够支撑长期更新。
这个站点本身就是一个静态站点。没有数据库,没有服务端渲染,也没有前端路由。 每次写完文章,运行一个脚本生成 HTML,再同步到服务器。整套流程很小,但足够稳定。
一、目录结构
site/
├── src/
│ ├── layout.html
│ ├── index.html
│ ├── articles/
│ │ └── first-post.html
│ └── assets/
│ └── style.css
├── build.mjs
└── dist/
src 放源文件和模板,dist 是构建产物,部署时只同步 dist。
产物目录不手工修改,避免源文件和线上文件不一致。
二、用模板拼装公共部分
页头、页脚和导航在每个页面重复,改动一次就要改很多文件。我用一个非常简单的占位符替换来处理:
模板里写 {{CONTENT}},构建时把页面正文填进去。
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { dirname, join } from 'node:path';
const layout = await readFile('src/layout.html', 'utf8');
async function render(src, dest) {
const body = await readFile(src, 'utf8');
const html = layout.replace('{{CONTENT}}', body);
await mkdir(dirname(dest), { recursive: true });
await writeFile(dest, html);
}
await render('src/index.html', 'dist/index.html');
await render('src/articles/first-post.html', 'dist/articles/first-post.html');
这里没有模板引擎,也没有转义逻辑。因为输入全部来自自己维护的文件,不需要处理不可信内容。 如果要支持用户提交的内容,就必须换成会正确转义的模板方案。
三、给静态资源加版本
浏览器会缓存 CSS 和图片,更新后用户可能还看到旧样式。最简单的方式是在文件名或查询串里带上内容哈希。
import { createHash } from 'node:crypto';
const css = await readFile('src/assets/style.css');
const hash = createHash('sha256').update(css).digest('hex').slice(0, 8);
await writeFile(`dist/assets/style.${hash}.css`, css);
页面里引用带哈希的文件名,就可以给静态资源设置很长的缓存时间。 HTML 本身保持短缓存或协商缓存,这样新版本能及时生效。
四、本地预览
预览不需要复杂工具,Node 自带的静态服务器就够了。重点是要按真实路径访问,确认相对路径和绝对路径都没有问题。
npx serve dist
# 或者
python3 -m http.server 8080 --directory dist
我通常会在预览时检查三件事:导航链接是否可达、移动端宽度下是否横向溢出、控制台是否有 404。 这三项能覆盖大部分低级问题。
五、部署时做摘要校验
静态站点可以用 rsync 同步,再用摘要确认线上文件与本地一致。保留服务器上的验证文件,
不要把整个目录删掉重传。
rsync -av --delete \
--exclude 'MP_verify_*.txt' \
dist/ deploy@example.com:/srv/site/
find dist -type f -print0 | sort -z | xargs -0 sha256sum > dist.sha256
--delete 会删除目标目录中多余的文件,所以排除规则要提前确认。
第一次部署前,先做一次备份,确认可以回滚。
六、缓存与压缩交给 Nginx
静态文件不需要应用层处理。让 Nginx 负责 gzip、缓存头和 404 页面,构建脚本只关心内容。
location /assets/ {
expires 30d;
add_header Cache-Control "public, immutable";
}
location / {
try_files $uri $uri.html $uri/ =404;
}
try_files 里加上 $uri.html,可以让 /about 这样的地址
对应到 about.html,链接写起来更干净。
七、什么时候需要更重的方案
当站点需要评论、搜索、多作者协作或频繁的结构化内容时,手工维护 HTML 会变得吃力。 那时候再引入生成器或内容管理系统也不迟。工具应该跟着需求走,而不是一开始就堆上去。