Nhà phát triểnAdapters
fs-build (site tĩnh self-host)
Site tĩnh self-host trên VPS — ghi .md vào content, kích build, phục vụ dist mới.
Dùng khi: site tĩnh (Astro/Hugo/Eleventy) self-host trên máy chủ riêng (VPS, không deploy qua GitHub CI). Bạn có một process thường trú (Node/PM2, PHP, systemd).
Endpoint nhận bài → ghi .md vào thư mục content → kích build → nginx phục vụ dist/ mới.
Chuẩn bị
- Một service nhỏ (Express/Fastify) chạy cạnh site, hoặc một route trong app runtime sẵn có.
- Env:
CITELY_WEBHOOK_SECRET,CONTENT_DIR=/var/www/site/src/content/blog,BUILD_CMD="npm --prefix /var/www/site run build".
Handshake (GET)
{
"citely": "receiver",
"protocol": 1,
"profile": "static-fs",
"storageAdapter": "fs-build",
"accepts": ["markdown"],
"locales": ["vi"],
"async": true
}Nhận bài (POST) — Node/Express
import { exec } from 'node:child_process';
import { writeFile } from 'node:fs/promises';
import express from 'express';
const app = express();
app.use(express.raw({ type: '*/*' })); // cần RAW body để verify chữ ký
app.get('/api/citely/ingest', (_req, res) =>
res.json({
citely: 'receiver', protocol: 1, profile: 'static-fs',
storageAdapter: 'fs-build', accepts: ['markdown'], locales: ['vi'], async: true,
}));
app.post('/api/citely/ingest', async (req, res) => {
const raw = req.body.toString('utf8');
const h = req.headers as Record<string, string>;
if (!verify(process.env.CITELY_WEBHOOK_SECRET!, raw, h)) {
return res.status(401).json({ error: 'CPP_SIG_INVALID' });
}
const a = JSON.parse(raw);
if (a.version !== 1) {
return res.status(400).json({ error: 'CPP_VERSION_UNSUPPORTED' });
}
if (a.test === true) {
return res.status(200).json({ ok: true });
}
try {
const fm = toFrontmatter(a); // map field CPP → Zod repo
await writeFile(`${process.env.CONTENT_DIR}/${a.slug}.md`, `---\n${fm}\n---\n\n${a.markdown}\n`);
} catch (e) {
return res.status(500).json({ error: 'CPP_STORAGE_FAILED', message: String(e) });
}
// Kích build BẤT ĐỒNG BỘ — không chờ build xong trong request (tránh timeout).
exec(process.env.BUILD_CMD!, err => err && console.error('build failed', err));
return res.status(202).json({ id: a.slug, url: `https://your-site.com/blog/${a.slug}`, status: 'building' });
});verify = hàm Standard Webhooks.
Đừng chạy build đồng bộ trong request. Build vài chục giây → Citely timeout. Trả 202 building NGAY, build nền; Citely poll url tới khi live (chi tiết).
Lưu ý
- Chống race build: nhiều bài về dồn dập → debounce build (ghi hết file rồi build một lần).
- An toàn lệnh:
BUILD_CMDlà hằng số, không ghép chuỗi từ payload → tránh command injection. Sanitizeslug(chặn..,/) trước khi ghép path.