Citely Docs
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 contentkí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_CMD là hằng số, không ghép chuỗi từ payload → tránh command injection. Sanitize slug (chặn .., /) trước khi ghép path.

On this page