/* 创建一篇新文章 * * 用法: * pnpm new-post "文章标题" * pnpm new-post "树莓派装 Docker" --category 教程 --tags Docker,树莓派 * pnpm new-post "读《XX》有感" --category 思考 --description "摘要" --pinned * pnpm new-post "标题" --file 文件名 --slug my-custom-slug --category 建站 * * 不加 --category 时文件放在 src/content/posts/ 根目录; * 加了则同时写进 frontmatter 的 category 并放进同名子目录,和现有文章结构一致。 */ import fs from "node:fs" import path from "node:path" const POSTS_DIR = "./src/content/posts" function today() { const d = new Date() const p = (n) => String(n).padStart(2, "0") return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}` } const args = process.argv.slice(2) if (args.length === 0 || args[0] === "--help" || args[0] === "-h") { // 无参数进来时先说清楚原因:下面 pnpm 还会补一句 // "[ELIFECYCLE] Command failed with exit code 1",不点明的话很像脚本崩了 if (args.length === 0) { console.log('缺少文章标题,没有创建任何文件。\n正确写法:pnpm new-post "树莓派装 Docker 全程记录" --category 教程\n') } console.log(`创建一篇新文章 用法: pnpm new-post "文章标题" [选项] 选项: --category <分类> 分类名,同时决定存放的子目录 --file <文件名> 自定义文件名(默认用标题) --tags <标签,标签> 标签,逗号分隔 --slug <英文主题> 通常使用文章标题的英文缩写,Astro博客自带的属性 --description <摘要> 摘要,用于 SEO 和列表卡片 --image <路径> 封面图,public 下的绝对路径 --pinned 置顶 --draft 存为草稿(构建时不会输出) 示例: pnpm new-post "树莓派装 Docker" --category 教程 --tags Docker,树莓派`) process.exit(args.length === 0 ? 1 : 0) } const title = args[0] const opts = { category: "", file: "", tags: "", description: "", image: "", pinned: false, draft: false, } for (let i = 1; i < args.length; i++) { const arg = args[i] if (arg === "--pinned") { opts.pinned = true continue } if (arg === "--draft") { opts.draft = true continue } if (!arg.startsWith("--")) { console.error(`错误:无法识别的参数 "${arg}"`) process.exit(1) } const key = arg.slice(2) if (!(key in opts)) { console.error(`错误:未知选项 --${key}`) process.exit(1) } const value = args[i + 1] if (value === undefined || value.startsWith("--")) { console.error(`错误:--${key} 缺少值`) process.exit(1) } opts[key] = value i++ } /** 文件名里不能出现 / : 等字符,统一替换掉 */ const safeName = (s) => s .replace(/[/\\:*?"<>|]/g, "") .replace(/\s+/g, "-") .trim() const fileName = `${safeName(opts.file || title)}.md` const dir = opts.category ? path.join(POSTS_DIR, safeName(opts.category)) : POSTS_DIR const fullPath = path.join(dir, fileName) if (fs.existsSync(fullPath)) { console.error(`错误:${fullPath} 已存在`) process.exit(1) } fs.mkdirSync(dir, { recursive: true }) const list = (v) => v ? v .split(",") .map((s) => s.trim()) .filter(Boolean) : [] const tags = list(opts.tags) /** 双引号包裹并转义,避免标题里的冒号、引号破坏 YAML */ const q = (v) => `"${String(v).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` const frontmatter = [ `title: ${q(title)}`, `published: ${today()}`, // 只写日期,YAML 按 UTC 零点解析,不会有时区偏移 opts.description ? `description: ${q(opts.description)}` : "", opts.image ? `image: ${q(opts.image)}` : "", tags.length > 0 ? `tags:\n${tags.map((t) => ` - ${q(t)}`).join("\n")}` : "tags: []", `category: ${q(opts.category)}`, opts.pinned ? "pinned: true" : "", opts.draft ? "draft: true" : "", ] .filter(Boolean) .join("\n") fs.writeFileSync(fullPath, `---\n${frontmatter}\n---\n\n在这里写正文。\n`, "utf8") console.log(`已创建文章: ${fullPath}`) if (opts.draft) console.log("状态:草稿(draft: true),构建时不会输出。写完记得删掉这一行。") console.log("运行 pnpm dev 可以在浏览器里实时预览。")