#!/usr/bin/env node // Publish seam: write a schema-valid blog post into src/content/blog/. // This is what an IaC/CI step calls to turn an event into a post + a commit. // // Usage (flags): // node scripts/new-post.mjs --title "My post" --summary "..." \ // --tags "kubernetes,gpu" [--date 2026-06-18] [--draft] \ // [--slug custom-slug] [--body "markdown..."] [--bodyFile notes.md] [--force] // // Usage (event JSON — for pipelines): // node scripts/new-post.mjs --json '{"title":"...","summary":"...","tags":["x"],"body":"..."}' // cat event.json | node scripts/new-post.mjs --stdin // // Exits non-zero on invalid input so a malformed pipeline event fails the build. import { parseArgs } from "node:util"; import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"; import { fileURLToPath } from "node:url"; const BLOG_DIR = fileURLToPath(new URL("../src/content/blog/", import.meta.url)); const { values } = parseArgs({ options: { title: { type: "string" }, summary: { type: "string" }, tags: { type: "string" }, date: { type: "string" }, slug: { type: "string" }, body: { type: "string" }, bodyFile: { type: "string" }, draft: { type: "boolean", default: false }, force: { type: "boolean", default: false }, json: { type: "string" }, stdin: { type: "boolean", default: false }, }, allowPositionals: false, }); function die(msg) { console.error(`new-post: ${msg}`); process.exit(1); } // --- Resolve the post fields from flags and/or a JSON event ---------------- let event = {}; if (values.stdin) { try { event = JSON.parse(readFileSync(0, "utf8")); } catch (e) { die(`could not parse JSON from stdin: ${e.message}`); } } else if (values.json) { try { event = JSON.parse(values.json); } catch (e) { die(`could not parse --json: ${e.message}`); } } const title = values.title ?? event.title; const summary = values.summary ?? event.summary; const rawTags = values.tags ?? event.tags; const date = values.date ?? event.date ?? new Date().toISOString().slice(0, 10); const draft = values.draft || Boolean(event.draft); let body = values.body ?? event.body; if (values.bodyFile) body = readFileSync(values.bodyFile, "utf8"); // --- Validate (mirror the zod schema; fail fast) --------------------------- if (!title || typeof title !== "string") die("a --title is required"); if (!summary || typeof summary !== "string") die("a --summary is required"); if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) die(`date must be YYYY-MM-DD (got "${date}")`); const tags = Array.isArray(rawTags) ? rawTags : typeof rawTags === "string" ? rawTags.split(",").map((t) => t.trim()).filter(Boolean) : []; const slug = values.slug ?? event.slug ?? title .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 80); if (!slug) die("could not derive a slug from the title; pass --slug"); // --- Compose the file ------------------------------------------------------ const yaml = (s) => JSON.stringify(String(s)); // safe double-quoted YAML scalar const frontmatter = [ "---", `title: ${yaml(title)}`, `date: ${date}`, `summary: ${yaml(summary)}`, `tags: [${tags.map(yaml).join(", ")}]`, `draft: ${draft}`, "---", "", ].join("\n"); const content = frontmatter + (body?.trim() ? `${body.trim()}\n` : `Write the post here.\n`); mkdirSync(BLOG_DIR, { recursive: true }); const file = `${BLOG_DIR}${slug}.md`; if (existsSync(file) && !values.force) { die(`${slug}.md already exists (use --force to overwrite)`); } writeFileSync(file, content, "utf8"); console.log(`wrote src/content/blog/${slug}.md${draft ? " (draft)" : ""}`);