Astro Content Collections 不只是整理 Markdown 的工具,也可以充当自动发布流程中的类型门禁。本文会让脚本生成的内容先通过 Schema 和生产构建,再由 GitHub Actions 提交并部署,任何不合规数据都会直接终止流水线。

先明确流水线的边界

手工写文章时,标题缺失、日期格式错误通常很快就能被发现;内容改由脚本、模型或外部接口生成后,错误更容易批量进入仓库。例如 tags 被输出成字符串、发布日期无法解析,或者草稿字段写成了 "no"

这条流水线采用以下顺序:

  1. 定时运行内容生成脚本;
  2. 将 Markdown 写入 Astro 内容目录;
  3. 加载 Content Collection,并执行 Schema 校验;
  4. 运行完整的生产构建;
  5. 仅在构建成功后提交内容并部署站点。

顺序很重要。如果先提交再构建,坏数据已经进入主分支;如果跳过生产构建,只单独检查文件格式,又可能遗漏页面渲染和静态路由阶段的问题。

环节主要职责失败后的结果
生成脚本获取或组装原始内容不产生提交
Collection Schema校验字段、类型和约束Astro 构建失败
页面构建验证查询、路由与渲染不提交、不部署
Git 提交保存已验证的内容保留可审计历史
Pages 部署发布本次构建产物线上版本不变

用 Content Collection 建立类型门禁

下面使用 Astro 的 Content Layer API。先创建项目并安装依赖:

npm create astro@latest astro-content-pipeline
cd astro-content-pipeline
npm install

在项目根目录创建 src/content.config.ts

import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const posts = defineCollection({
  loader: glob({
    base: './src/content/posts',
    pattern: '**/*.{md,mdx}',
  }),
  schema: z.object({
    title: z.string().min(1).max(80),
    description: z.string().min(20).max(200),
    publishedAt: z.coerce.date(),
    updatedAt: z.coerce.date().optional(),
    draft: z.boolean().default(false),
    tags: z.array(z.string().min(1)).min(1).max(5),
  }),
});

export const collections = { posts };

这里不只检查字段是否存在,还限制了文本长度、标签数量和日期类型。z.coerce.date() 允许 Markdown 中使用 ISO 日期字符串,集合加载后则得到真正的 Date 对象。

需要注意,Schema 能确认结构正确,却不能判断事实是否真实,也不能替代敏感词、链接可用性或内容质量检查。后续如果有这些需求,应增加独立脚本,而不是把所有规则都塞进一个复杂 Schema。

编写可重复执行的内容生成脚本

创建 scripts/generate-content.mjs。这个示例按 UTC 日期生成一篇日报,同一天重复运行会覆盖同一个文件,避免产生重复文章。实际项目可以把正文替换为数据库查询、RSS 聚合或经过审核的模型输出。

import { mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';

const outputDirectory = join(process.cwd(), 'src/content/posts');
const today = new Date().toISOString().slice(0, 10);
const publishedAt = `${today}T00:00:00.000Z`;
const title = `${today} 自动化内容日报`;
const description = `汇总 ${today} 的项目更新,并记录本次自动化内容流水线的执行结果。`;

const frontmatter = [
  '---',
  `title: ${JSON.stringify(title)}`,
  `description: ${JSON.stringify(description)}`,
  `publishedAt: ${JSON.stringify(publishedAt)}`,
  'draft: false',
  'tags:',
  '  - Automation',
  '  - Astro',
  '---',
].join('\n');

const body = `${frontmatter}

这篇内容由仓库中的生成脚本创建。

## 今日记录

- 内容文件按日期生成;
- Astro Schema 负责字段校验;
- 生产构建成功后才允许发布。
`;

await mkdir(outputDirectory, { recursive: true });
await writeFile(join(outputDirectory, `${today}-daily.md`), body, 'utf8');

console.log(`Generated post for ${today}`);

JSON.stringify() 生成的字符串可以直接作为 YAML 标量使用,比手工拼接引号更稳妥。不过对于来自外部的不可信正文,仍要根据使用场景处理 HTML、MDX 表达式和其他潜在输入。

package.jsonscripts 中加入命令:

{
  "scripts": {
    "dev": "astro dev",
    "build": "astro build",
    "preview": "astro preview",
    "content:generate": "node scripts/generate-content.mjs"
  }
}

现在可以本地验证完整门禁:

npm run content:generate
npm run build

尝试把生成文件里的 tags 改成 tags: Astro,再次构建时,集合 Schema 会因为期望数组却收到字符串而报错。这个失败状态正是 GitHub Actions 用来阻止提交和部署的信号。

查询并渲染已经验证的内容

为了让内容真正生成静态页面,创建 src/pages/posts/[...id].astro

---
import type { CollectionEntry } from 'astro:content';
import { getCollection, render } from 'astro:content';

export async function getStaticPaths() {
  const posts = await getCollection('posts', ({ data }) => !data.draft);

  return posts.map((post) => ({
    params: { id: post.id },
    props: { post },
  }));
}

type Props = {
  post: CollectionEntry<'posts'>;
};

const { post } = Astro.props;
const { Content } = await render(post);
---

<html lang="zh-CN">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <meta name="description" content={post.data.description} />
    <title>{post.data.title}</title>
  </head>
  <body>
    <main>
      <h1>{post.data.title}</h1>
      <time datetime={post.data.publishedAt.toISOString()}>
        {post.data.publishedAt.toLocaleDateString('zh-CN')}
      </time>
      <Content />
    </main>
  </body>
</html>

getCollection() 返回的数据已经通过 Schema,因此页面中可以按 Date 使用 publishedAt。草稿也在生成静态路由前被过滤。若页面组件引用了不存在的属性,生产构建同样会失败,不会进入后续提交步骤。

用 GitHub Actions 定时生成并发布

创建 .github/workflows/content-pipeline.yml

name: Content pipeline

on:
  schedule:
    - cron: '15 1 * * *'
  workflow_dispatch:

permissions:
  contents: write
  pages: write
  id-token: write

concurrency:
  group: github-pages
  cancel-in-progress: false

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          ref: main

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Generate content
        run: npm run content:generate

      - name: Validate and build
        run: npm run build

      - name: Commit validated content
        run: |
          if git diff --quiet -- src/content/posts; then
            echo "No content changes"
            exit 0
          fi
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git add src/content/posts
          git commit -m "chore(content): generate scheduled content"
          git push origin HEAD:main

      - name: Configure GitHub Pages
        uses: actions/configure-pages@v5

      - name: Upload Pages artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: dist

      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

Cron 使用 UTC 时间,上例每天 01:15 执行。仓库默认分支不是 main 时,需要同步修改检出和推送目标。还要在仓库 Settings 的 Pages 页面中将发布来源设为 GitHub Actions;部署到项目子路径时,则应按 Astro 的 GitHub Pages 部署文档配置 sitebase

关键门禁是 Validate and build 位于提交之前。Shell 命令返回非零状态后,当前 Job 会立即停止,后面的 Git 提交和 Pages 部署都不会运行。npm ci 还会严格使用锁文件,减少本地与 CI 依赖版本不一致的问题。

总结

这套流程的核心不是“定时写一个 Markdown”,而是让自动生成内容遵守与手工内容相同、甚至更严格的发布规则:

  • 用 Astro Content Collection Schema 约束标题、日期、草稿状态和标签;
  • 让生成脚本可重复执行,避免定时任务制造重复内容;
  • 通过 astro build 同时检查集合数据、页面查询和静态渲染;
  • 只有构建成功后才提交文件并部署 GitHub Pages;
  • 将事实核验、安全审查等能力保留为独立门禁,不夸大 Schema 的作用。

当内容来源继续增加时,还可以在构建前插入链接检查、重复度检查或人工审批 Job。无论增加多少步骤,都应坚持同一原则:先验证,再进入版本历史,最后发布到线上。