Pages from a CMS in Stati

Write the CMS's posts as Markdown before the build starts. A hook is too late.

SNP 126 #A25 Filed stati node

javascript
// scripts/fetch-posts.mjs: the CMS's posts as Markdown files in site/news/
import { writeFile } from 'node:fs/promises';

const response = await fetch('https://cms.example.com/api/posts', {
  headers: { Authorization: `Bearer ${process.env.CMS_TOKEN}` },
});
if (!response.ok) throw new Error(`The CMS answered ${response.status}`);
const posts = await response.json();

for (const post of posts) {
  // The file name becomes the URL as it is, so allow only what belongs in one
  if (!/^[a-z0-9-]+$/.test(post.slug)) throw new Error(`Unusable slug: ${post.slug}`);
  // JSON is valid YAML: JSON.stringify quotes and escapes every value
  const markdown = `---
title: ${JSON.stringify(post.title)}
description: ${JSON.stringify(post.summary)}
publishedAt: ${JSON.stringify(post.publishedAt)}
tags: ${JSON.stringify(post.tags)}
---

${post.body}
`;
  await writeFile(`site/news/${post.slug}.md`, markdown);
}
console.log(`Wrote ${posts.length} posts`);
json
{
  "scripts": {
    "prebuild": "node scripts/fetch-posts.mjs",
    "build": "stati build"
  }
}

npm runs prebuild before every npm run build. It can't be a hook: Stati finds the pages before any hook runs, so a Markdown file written in beforeAll only becomes a page in the build after.

Keep the generated files out of git, with site/news/* and !site/news/index.md (the listing) in .gitignore. A build that starts from a clean checkout then never shows a post the CMS has deleted. The token only lives in this script and CI's secrets, never in the built site.

In the Stati docs: Integration with a CMS and Hook lifecycle.