新增 new-post 脚本与操作指南文档
- scripts/new-post.mjs + pnpm new-post:按标题建文章,支持 --category/--tags/ --description/--image/--file/--pinned/--draft,标题里的冒号引号会正确转义 - docs/操作指南.md:写文章、发偶得、加相册、预览、检查、部署的完整流程 - 修复 astro check 报出的 2 个类型错误(gallery 索引页 querySelector 未指定 HTMLElement,导致 dataset 不可用)。现在 pnpm check 为 0 error
This commit is contained in:
@@ -13,6 +13,8 @@
|
|||||||
|
|
||||||
评论配置在 `src/data/comments.ts`。
|
评论配置在 `src/data/comments.ts`。
|
||||||
|
|
||||||
|
> 日常操作(写文章、发偶得、加照片、部署)看 **[docs/操作指南.md](docs/操作指南.md)**,那份更偏"照着做";本文偏项目结构与原理。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|||||||
+240
@@ -0,0 +1,240 @@
|
|||||||
|
# 操作指南
|
||||||
|
|
||||||
|
这个博客的日常操作:写文章、写偶得、加照片、本地预览、部署上线。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 首次准备
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/tianxuetao/文档/VermilionVoid-main
|
||||||
|
pnpm install # 只需在第一次、或依赖变动后执行
|
||||||
|
```
|
||||||
|
|
||||||
|
平时不需要重复装依赖。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 本地预览(写得最多的一步)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
打开 **http://localhost:4321**。改任何文件浏览器会自动刷新,不用重启。
|
||||||
|
|
||||||
|
停止:在终端按 `Ctrl + C`。
|
||||||
|
|
||||||
|
> 端口被占用时 Astro 会自动换到 4322、4323……看终端输出的那行 `Local` 网址即可。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 写一篇正式文章
|
||||||
|
|
||||||
|
### 建文件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm new-post "树莓派装 Docker 全程记录" --category 教程 --tags Docker,树莓派
|
||||||
|
```
|
||||||
|
|
||||||
|
常用选项:
|
||||||
|
|
||||||
|
| 选项 | 作用 |
|
||||||
|
|---|---|
|
||||||
|
| `--category 教程` | 分类,同时决定放进哪个子目录 |
|
||||||
|
| `--tags Docker,树莓派` | 标签,逗号分隔 |
|
||||||
|
| `--description "摘要"` | 摘要,用于 SEO 和列表卡片 |
|
||||||
|
| `--image /assets/images/xxx.webp` | 封面图 |
|
||||||
|
| `--file my-slug` | 自定义文件名(默认用标题) |
|
||||||
|
| `--pinned` | 置顶 |
|
||||||
|
| `--draft` | 草稿,构建时不会输出 |
|
||||||
|
|
||||||
|
生成的文件长这样:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
title: "树莓派装 Docker 全程记录"
|
||||||
|
published: 2026-09-14
|
||||||
|
description: "摘要"
|
||||||
|
tags:
|
||||||
|
- "Docker"
|
||||||
|
- "树莓派"
|
||||||
|
category: "教程"
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
### 写正文
|
||||||
|
|
||||||
|
直接在 `---` 下面往下写 Markdown。支持:
|
||||||
|
|
||||||
|
- 标题、列表、表格、引用、代码块(带行号和复制按钮)
|
||||||
|
- 数学公式:`$行内$` 和 `$$独立成段$$`
|
||||||
|
- 提示框:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
> [!NOTE]
|
||||||
|
> 这是一条提示。也可以用 TIP / IMPORTANT / CAUTION / WARNING
|
||||||
|
```
|
||||||
|
|
||||||
|
- 图片:``
|
||||||
|
|
||||||
|
### 不想用脚本
|
||||||
|
|
||||||
|
手动在 `src/content/posts/` 下建 `.md` 文件也可以,**只有 `title` 和 `published` 是必填的**,其余字段都能省略。
|
||||||
|
|
||||||
|
### 加密文章(可选)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
encrypted: true
|
||||||
|
password: "你的密码"
|
||||||
|
```
|
||||||
|
|
||||||
|
正文会被加密,需要输密码才能阅读。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 写一条「偶得」
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm new-thought "换了散热,温度降了 15 度。" --location 张掖 --tags 硬件
|
||||||
|
```
|
||||||
|
|
||||||
|
| 选项 | 作用 |
|
||||||
|
|---|---|
|
||||||
|
| `--title` | 可选标题 |
|
||||||
|
| `--location` | 地点,显示在日期旁 |
|
||||||
|
| `--weather` | 天气 |
|
||||||
|
| `--images a.png,b.png` | 配图,逗号分隔 |
|
||||||
|
| `--tags 生活,硬件` | 标签 |
|
||||||
|
| `--pinned` | 置顶 |
|
||||||
|
|
||||||
|
> ⚠️ **`published` 一定要带时区**(脚本会自动写成 `2026-09-14T12:36:47.215Z`)。
|
||||||
|
> 手写成不带时区的 `2026-09-14 12:36`,YAML 会按 UTC 解析,**显示的日期会差一天**。
|
||||||
|
|
||||||
|
偶得页有两种模式:**专注**(一次一条,←/→ 切换)和**时间线**(按年份分组 + 标签筛选)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 加照片到相册
|
||||||
|
|
||||||
|
照片放在 `public/gallery/<相册id>/`,**构建时自动扫描目录**,不用在配置里逐个登记。
|
||||||
|
|
||||||
|
加一个新相册 = 两步:
|
||||||
|
|
||||||
|
1. 建目录并放照片:`public/gallery/我的相册/`
|
||||||
|
2. 在 `src/data/gallery.ts` 的 `albums` 里加一项:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
id: "我的相册", // 必须和目录名一致
|
||||||
|
name: "相册显示名",
|
||||||
|
description: "一句话描述",
|
||||||
|
location: "甘肃-张掖", // 可选
|
||||||
|
date: "2026-09-14", // 可选
|
||||||
|
tags: ["旅行"], // 可选,用于索引页筛选
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
封面优先级:`cover` 字段 > 目录里的 `cover.*` 文件 > 第一张图。
|
||||||
|
|
||||||
|
> 📏 **放进去之前先压图。** `public/` 下的资源由服务器原样下发,**不经过 Astro 优化**。
|
||||||
|
> 一张 4K 原图可能十几 MB,会让页面加载非常慢。建议先压成 WebP 再放。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 检查与构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm check # 类型 + 内容校验,frontmatter 写错会在这里报出来
|
||||||
|
pnpm build # 构建到 dist/,并建立搜索索引
|
||||||
|
pnpm preview # 本地预览构建产物(搜索功能只有这里才生效)
|
||||||
|
```
|
||||||
|
|
||||||
|
写完一篇后建议跑一次 `pnpm check`,能提前发现 frontmatter 字段写错。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 部署上线
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
它会构建一次,然后 rsync 到树莓派和云服务器,覆盖生产前会问你一句确认。
|
||||||
|
|
||||||
|
| 命令 | 用途 |
|
||||||
|
|---|---|
|
||||||
|
| `./deploy.sh` | 构建 + 部署两个目标 |
|
||||||
|
| `./deploy.sh -y` | 同上,跳过确认(适合脚本) |
|
||||||
|
| `./deploy.sh --pi-only` | 只推树莓派,先在内网看效果 |
|
||||||
|
| `./deploy.sh --cloud-only` | 只推云服务器(正式上线) |
|
||||||
|
| `./deploy.sh --dry-run` | 空跑,只显示会同步哪些文件 |
|
||||||
|
| `./deploy.sh --skip-build` | 不重新构建,直接推现有 `dist/` |
|
||||||
|
|
||||||
|
推荐节奏:先 `--pi-only` 在树莓派上确认没问题,再 `--cloud-only` 上生产。
|
||||||
|
|
||||||
|
> 目标地址和 SSH 密钥在 `deploy.sh` 顶部的**配置区**。
|
||||||
|
> 脚本在构建失败或产物异常时会**直接中止**——因为推送用的是 `rsync --delete`,
|
||||||
|
> 拿一个残缺的 `dist/` 上去会清空线上目录。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 改站点信息
|
||||||
|
|
||||||
|
日常最常改的三个文件:
|
||||||
|
|
||||||
|
| 想改什么 | 改哪里 |
|
||||||
|
|---|---|
|
||||||
|
| 站名、域名、描述、主题色、统计 | `src/data/site.ts` |
|
||||||
|
| 昵称、签名、头像、GitHub/邮箱 | `src/data/profile.ts` |
|
||||||
|
| 首页打字机文案 | `src/components/hero.tsx` 顶部的 `phrases` |
|
||||||
|
|
||||||
|
主题色只改 `site.ts` 里的 `themeHue` 一个数字:`0` 红、`25` 朱红(当前)、`200` 青、`250` 蓝绿、`345` 粉。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 常见问题
|
||||||
|
|
||||||
|
**改了文件但浏览器没变**
|
||||||
|
先手动刷新。若仍无效,`Ctrl + C` 停掉再 `pnpm dev` 重启。
|
||||||
|
|
||||||
|
**构建时报 `ENOSPC: System limit for number of file watchers reached`**
|
||||||
|
系统的 inotify 监视器配额用完了,不是代码问题。页面照常访问,但热更新可能失效。
|
||||||
|
彻底解决需要放宽内核参数(要 sudo):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo sysctl fs.inotify.max_user_watches=524288
|
||||||
|
```
|
||||||
|
|
||||||
|
**偶得的日期差一天**
|
||||||
|
看第 3 节:`published` 必须带时区。
|
||||||
|
|
||||||
|
**图片不显示**
|
||||||
|
检查路径是否以 `/` 开头(`/assets/images/x.webp`),以及文件是否真在 `public/` 下对应位置。
|
||||||
|
注意 `public/` 里的文件路径就是 URL 路径,**没有 `public` 这一段**。
|
||||||
|
|
||||||
|
**想回退到之前的状态**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git log --oneline # 看提交
|
||||||
|
git reset --hard <提交号>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附:目录速查
|
||||||
|
|
||||||
|
```
|
||||||
|
src/content/posts/ 文章(按分类分子目录)
|
||||||
|
src/content/thoughts/ 偶得
|
||||||
|
src/content/studies/ 专题(目前为空)
|
||||||
|
src/data/site.ts 站点配置
|
||||||
|
src/data/profile.ts 个人信息
|
||||||
|
src/data/gallery.ts 相册配置
|
||||||
|
src/data/comments.ts 评论配置
|
||||||
|
src/components/hero.tsx 首页打字机文案
|
||||||
|
public/assets/images/ 文章配图
|
||||||
|
public/gallery/ 相册照片
|
||||||
|
scripts/ 辅助脚本
|
||||||
|
dist/ 构建产物(gitignore)
|
||||||
|
```
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
"build": "astro build && pagefind --site dist",
|
"build": "astro build && pagefind --site dist",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"check": "astro check",
|
"check": "astro check",
|
||||||
|
"new-post": "node scripts/new-post.mjs",
|
||||||
"new-thought": "node scripts/new-thought.mjs"
|
"new-thought": "node scripts/new-thought.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
/* 创建一篇新文章
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* pnpm new-post "文章标题"
|
||||||
|
* pnpm new-post "树莓派装 Docker" --category 教程 --tags Docker,树莓派
|
||||||
|
* pnpm new-post "读《XX》有感" --category 思考 --description "摘要" --pinned
|
||||||
|
* pnpm new-post "标题" --file 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") {
|
||||||
|
console.log(`创建一篇新文章
|
||||||
|
|
||||||
|
用法:
|
||||||
|
pnpm new-post "文章标题" [选项]
|
||||||
|
|
||||||
|
选项:
|
||||||
|
--category <分类> 分类名,同时决定存放的子目录
|
||||||
|
--file <文件名> 自定义文件名(默认用标题)
|
||||||
|
--tags <标签,标签> 标签,逗号分隔
|
||||||
|
--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 可以在浏览器里实时预览。")
|
||||||
@@ -124,7 +124,7 @@ const allTags = getAllAlbumTags(albums)
|
|||||||
<script>
|
<script>
|
||||||
// 标签筛选。ClientRouter 切页后需要重新绑定,所以监听 astro:page-load。
|
// 标签筛选。ClientRouter 切页后需要重新绑定,所以监听 astro:page-load。
|
||||||
const initAlbumFilter = () => {
|
const initAlbumFilter = () => {
|
||||||
const filter = document.querySelector("[data-album-filter]")
|
const filter = document.querySelector<HTMLElement>("[data-album-filter]")
|
||||||
if (!filter || filter.dataset.bound === "true") return
|
if (!filter || filter.dataset.bound === "true") return
|
||||||
filter.dataset.bound = "true"
|
filter.dataset.bound = "true"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user