Inkstone · blog

从空域名到可写博客:Astro、Cloudflare Pages 与 Sveltia CMS 的完整部署记录

5,035 words 15 min read #Cloudflare#Astro#Sveltia CMS#GitHub#Obsidian

这篇文章最初写于 2026-06-04,当时目标很直接:域名已经托管在 Cloudflare 上,但打开还是空的;本地有一套 Obsidian 笔记,希望以后能挑选其中一部分文章发布到公开博客;写作设备不只一台,iPhone、iPad 和 MacBook 都应该能参与写作和发布。

最初落地的主干架构没有变:

Obsidian / 本地编辑器 / Sveltia CMS
GitHub 私有仓库保存源码、Markdown 和上传资源
Cloudflare Pages 拉取 main 分支并构建 Astro
Cloudflare 自定义域名访问

但后面真实运行了一段时间后,博客已经不只是“能打开、能写文章”。现在它还补上了几个更接近生产站点的环节:

  • Sveltia CMS 恢复成 GitHub OAuth 登录,token 只作为应急 fallback。
  • 文章 frontmatter 有稳定 schema,支持分类、系列、评论、封面图和侧栏。
  • draft: true 不只从 Astro 列表过滤,还会生成 Cloudflare Pages _redirects,避免历史 HTML 继续被访问。
  • npm testnpm run build 变成发布前守门。
  • Cloudflare cache purge 用 GitHub Actions 自动做。
  • 评论系统用 Giscus,评论数据放在一个公开 Discussions 仓库,主内容仓库仍保持 private。
  • 站点还支持 Gallery,但照片原图不进仓库,只保存网页展示图和 Markdown 元数据。

所以这篇文章也重新整理成一篇完整部署手册:如果你想搭一个类似的个人博客,可以按这里的顺序复刻;如果只是看架构取舍,也可以跳着读。

最终架构

当前站点可以拆成五层。

写作层
- Obsidian:原始笔记和草稿池
- 本地编辑器:批量整理、改代码、迁移文章
- Sveltia CMS:浏览器、iPad、临时设备上的 Git-based CMS
内容层
- src/content/blog:博客 Markdown / MDX
- src/content/gallery:照片条目 Markdown
- public/uploads:公开图片和附件
- src/config/site.toml:站点配置、导航、评论、搜索
构建层
- Astro content collections 校验 frontmatter
- Astro 静态生成页面
- Pagefind 生成站内搜索索引
- scripts/generate-draft-redirects.mjs 生成 draft 隐藏规则
- scripts/validate-site.mjs 做发布前校验
部署层
- GitHub 私有仓库 lxy1992/inkstone
- Cloudflare Pages 连接 main 分支
- Build command: npm run build
- Output directory: dist
访问层
- lvxinyan.com 作为主域名
- www.lvxinyan.com 301 跳到主域名
- /admin/ 加载 Sveltia CMS
- GitHub Actions 发布后清理 Cloudflare 缓存

这里没有传统数据库。文章、图片、页面结构和发布历史都跟着 Git 走。对个人博客来说,这是一个很重要的取舍:可迁移性比后台功能更重要。

为什么不是 WordPress 或 Obsidian Publish

WordPress 的优势是完整的后台和成熟生态,但它也带来数据库、插件、安全更新、登录后台和服务端运维。对一个个人技术博客来说,运行面偏大。

Obsidian Publish 的优势是和 Obsidian 原生体验贴合,但它更像“公开笔记库”。这里的目标不是把整个 vault 变成数字花园,而是把挑选后的内容整理成独立博客文章。公开文章应该是整理过的结果,不是私人笔记库的镜像。

Quartz / Digital Garden 适合 Obsidian 场景,但这次还希望首页有个人主页属性,并且可以自由调整视觉、路由、RSS、评论、搜索、CMS 和图片展示,所以最终选了 Astro。

Astro + Cloudflare Pages + Sveltia CMS 的组合,本质是:

静态站点生成器 + Git 托管内容 + 免费静态部署 + 浏览器写作后台

它不是功能最多的方案,但长期维护成本低。

仓库结构

当前仓库的关键结构是:

.
├── astro.config.mjs
├── package.json
├── public/
│ ├── _redirects
│ ├── admin/
│ │ └── config.yml
│ ├── images/
│ └── uploads/
├── scripts/
│ ├── generate-draft-redirects.mjs
│ ├── import-gallery.mjs
│ ├── new-content.ts
│ └── validate-site.mjs
├── src/
│ ├── config/
│ │ └── site.toml
│ ├── content.config.ts
│ ├── content/
│ │ ├── blog/
│ │ ├── gallery/
│ │ └── projects/
│ ├── pages/
│ │ ├── admin/
│ │ ├── blog/
│ │ ├── gallery/
│ │ └── rss.xml.js
│ └── components/
└── .github/
└── workflows/
└── purge-cloudflare-cache.yml

如果你只是复刻一个最小博客,必须理解的目录只有四个:

  • src/content/blog/:博客文章。
  • public/uploads/:上传图片。
  • public/admin/config.yml:Sveltia CMS 配置。
  • src/content.config.ts:Astro content collection schema。

后面补上的 scripts/.github/workflows/ 不是为了炫技,而是为了让发布、draft、缓存、图片引用这些问题不靠记忆维护。

初始化 Astro 项目

新项目可以从 Astro 官方模板开始:

Terminal window
npm create astro@latest

这个站点现在要求 Node.js 不低于:

Node.js >= 22.12.0

常用依赖大致分几类:

astro 静态站点生成
@astrojs/rss RSS
@astrojs/sitemap sitemap
@astrojs/mdx MDX 支持
astro-expressive-code 代码块增强
remark-math / rehype-katex 数学公式
pagefind 静态站内搜索
tailwindcss 样式工具
sharp Gallery 图片处理

package.json 里最关键的是这些脚本:

{
"scripts": {
"dev": "ASTRO_TELEMETRY_DISABLED=1 astro dev",
"redirects:generate": "node scripts/generate-draft-redirects.mjs",
"test": "npm run redirects:generate && node scripts/validate-site.mjs",
"build": "npm run redirects:generate && ASTRO_TELEMETRY_DISABLED=1 astro check && ASTRO_TELEMETRY_DISABLED=1 astro build && pagefind --site dist --output-subdir pagefind --root-selector main --exclude-selectors \"[data-pagefind-ignore]\"",
"preview": "ASTRO_TELEMETRY_DISABLED=1 astro preview",
"post:new": "node --experimental-strip-types scripts/new-content.ts blog"
}
}

这里有两个设计点:

  1. npm test 不跑完整构建,但会生成 _redirects 并检查站点结构。
  2. npm run build 一定先生成 _redirects,再执行 Astro build 和 Pagefind。

这样 draft 隐藏规则不会变成一个“记得就跑、不记得就漏”的手工步骤。

Astro 配置

astro.config.mjs 的核心配置是站点地址、MDX、sitemap、代码块和 Markdown 插件:

import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
import { defineConfig } from 'astro/config';
import expressiveCode from 'astro-expressive-code';
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
export default defineConfig({
site: 'https://lvxinyan.com',
integrations: [
expressiveCode({
frames: {
showCopyToClipboardButton: true,
},
}),
mdx(),
sitemap(),
],
markdown: {
processor: unified({
remarkPlugins: [remarkMath],
rehypePlugins: [rehypeKatex],
}),
},
});

真实仓库里还处理了 GitHub Actions / GitHub Pages 的 SITE_URLSITE_BASE 兼容,但如果你只部署到 Cloudflare Pages 自定义域名,最小配置可以先固定 site

内容模型

Astro content collections 是这个博客的边界层。所有 Markdown 不是随便写 frontmatter,而是先经过 schema 校验。

博客文章支持这些字段:

{
title: string;
description?: string;
summary?: string;
date: Date;
updated?: Date;
draft?: boolean;
heroImage?: string;
showHeroImage?: boolean;
tags?: string[];
categories?: string[];
series?: string[];
comments?: boolean;
sidebar?: {
enable?: boolean;
toc?: boolean;
relatedPosts?: boolean;
};
}

当前约定是:文章必须有 descriptionsummary,最后都会归一成页面 description。

一篇完整文章的 frontmatter 可以这样写:

---
title: 'Example Post'
description: 'A short page description for SEO and article cards.'
date: '2026-06-04T12:00:00+08:00'
updated: '2026-06-12T12:20:00+08:00'
draft: false
heroImage: '/uploads/example-post/cover.png'
showHeroImage: true
tags:
- 'Astro'
- 'Cloudflare'
categories:
- 'Cloudflare / 建站'
series: []
comments: true
sidebar:
enable: true
toc: true
relatedPosts: true
---

几个字段的语义:

  • draft: true 表示不公开。
  • heroImage 只允许 /uploads/.../images/... 或远程 HTTP 图片。
  • showHeroImage: false 表示有封面图元数据,但文章页不展示顶部大图。
  • categories 适合少量归档分组。
  • tags 适合细粒度关键词。
  • series 用来串连续文章。
  • comments: false 可以关闭单篇文章评论。

这里的一个小坑是日期。静态构建、RSS、页面渲染可能会涉及时区转换。如果你只写 2026-06-04,在一些 UTC 语境里可能会出现前一天/后一天的展示差异。我的经验是给文章时间写明确的 +08:00,必要时用中午时间,避免跨日边界。

页面路由和 draft 过滤

博客详情页、列表页、分类页、系列页和 RSS 都只读取非 draft 内容。核心写法是:

const posts = await getCollection('blog', ({ data }) => !data.draft);

这一步负责“最新构建不生成 draft 页面”。但它不能解决所有问题。

Cloudflare Pages 作为静态托管平台,曾经出现过一个现象:一篇文章已经变成 draft: true,最新构建也不再生成它,但 custom domain 上的历史尾斜杠 URL 仍然能看到旧 HTML。普通的 Cloudflare Purge Everything 和 Cache Purge API 也没有立刻清掉。

这件事最后推动了一个更稳的设计:draft 隐藏不能只靠“最新构建不生成页面”,还要让部署产物显式告诉 Pages 这些 URL 不应该展示旧内容。

构建时生成 _redirects

Cloudflare Pages 支持部署产物里的 _redirects 文件。这个博客在构建前扫描 src/content/blog/,为每篇 draft 文章生成三条规则:

/blog/<slug> /.draft-hidden/blog/<slug> 302
/blog/<slug>/ /.draft-hidden/blog/<slug> 302
/blog/<slug>/index.html /.draft-hidden/blog/<slug> 302

目标路径 /.draft-hidden/... 在仓库里不存在,所以最终返回 404。用 302 是因为 draft 是可逆状态,文章重新公开后下一次构建会自动移除这些规则。

生成脚本的核心逻辑是:

const draftSlugs = walkFiles(blogDir)
.filter((filePath) => /\.(md|mdx)$/i.test(filePath))
.filter((filePath) => isDraft(readFrontmatter(readFileSync(filePath, 'utf8'))))
.map(toBlogSlug)
.sort();
const redirectLines = draftSlugs.flatMap((slug) => {
const target = `${draftTargetPrefix}/${slug}`;
return [
`/blog/${slug} ${target} 302`,
`/blog/${slug}/ ${target} 302`,
`/blog/${slug}/index.html ${target} 302`,
];
});

这不是为了代替 Astro 的 draft 过滤,而是补上 Cloudflare Pages 历史资源保留可能带来的边界问题。

Sveltia CMS 配置

/admin/ 页面本身非常薄,只加载 Sveltia CMS:

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="robots" content="noindex" />
<title>Inkstone CMS</title>
<script src="https://unpkg.com/@sveltia/cms/dist/sveltia-cms.js" defer></script>
</head>
<body>
<noscript>Inkstone CMS requires JavaScript.</noscript>
</body>
</html>

CMS 配置在 public/admin/config.yml。当前使用 Sveltia CMS Authenticator + GitHub OAuth 登录:

backend:
name: github
repo: lxy1992/inkstone
branch: main
base_url: https://sveltia-cms-auth.20210503blog.workers.dev
media_folder: public/uploads
public_folder: /uploads

最初我为了快速稳定发布,曾经把 CMS 切成 token 登录。那条路部署最简单,但手机、iPad 或临时设备上每次都要找一串很长的 token,长期体验不合适。当前站点恢复为 OAuth 登录,原因很直接:

  • 手机和临时设备上可以直接点 GitHub 授权。
  • token 不再是日常登录入口,只作为 OAuth Worker 出问题时的应急 fallback。
  • Sveltia CMS 保存文章的本质仍然是往 GitHub 提交 Markdown。
  • 代价是需要维护一个轻量的 Cloudflare Worker 和 GitHub OAuth App。

如果你也使用 private GitHub repo,又只有自己一个技术用户,fine-grained token 仍然是最省事的方案。但如果你需要频繁换设备登录,OAuth Worker 更符合 CMS 的使用体验。

Repository access: only selected repositories
Selected repository: <owner>/<blog-repo>
Permissions:
Contents: Read and write

不要把 token 写进仓库、文档、issue 或聊天记录。它只应该存在于你登录 CMS 的浏览器里,或者由密码管理器保存。

Blog collection

CMS 里的 Blog collection 对应 src/content/blog

collections:
- name: blog
label: Blog
folder: src/content/blog
create: true
slug: '{{year}}-{{month}}-{{day}}-{{slug}}'
fields:
- label: Title
name: title
widget: string
- label: Summary
name: summary
widget: text
- label: Date
name: date
widget: datetime
- label: Updated
name: updated
widget: datetime
required: false
- label: Hero Image
name: heroImage
widget: image
required: false
- label: Show Hero Image
name: showHeroImage
widget: boolean
default: true
required: false
- label: Tags
name: tags
widget: list
required: false
- label: Categories
name: categories
widget: list
required: false
- label: Series
name: series
widget: list
required: false
- label: Draft
name: draft
widget: boolean
default: false
required: false
- label: Comments
name: comments
widget: boolean
default: true
required: false
- label: Body
name: body
widget: markdown

CMS 保存后会产生一次 GitHub commit。Cloudflare Pages 看到 main 分支变化后自动构建。也就是说,CMS 并不是一个独立数据库后台,它只是一个更友好的 Git 写作界面。

当前站点还支持 Gallery:

src/content/gallery/
public/uploads/gallery/

Gallery 的设计原则是:仓库只保存网页展示图和 Markdown 元数据,不保存 NAS 原图、RAW 或超大 JPG。

CMS 里可以维护:

  • 标题
  • 描述
  • 日期
  • 展示图
  • 相册照片
  • 地点
  • 分类
  • 相机
  • 是否 featured
  • 是否 draft

如果要批量从 NAS 导入照片,可以用脚本生成多尺寸 WebP/JPG:

Terminal window
npm run gallery:import -- /Volumes/photos/export.jpg \
--title "Late light by the river" \
--date 2026-05-12 \
--location "Shanghai" \
--category City \
--camera "Fujifilm X100VI" \
--featured

如果你只想搭博客,可以先跳过 Gallery。它不是主架构必需品。

Cloudflare Pages 部署

Cloudflare Pages 连接 GitHub 仓库部署。当前站点的关键配置是:

Project name: inkstone
Production branch: main
Build command: npm run build
Build output directory: dist
Root directory: repository root
Production domain: https://lvxinyan.com
Pages hostname: https://inkstone-9ev.pages.dev

复刻时按这个顺序做:

  1. 把 Astro 项目推到 GitHub 仓库。
  2. 在 Cloudflare Pages 里选择 Connect to Git。
  3. 授权 Cloudflare 访问这个仓库。
  4. Production branch 选 main
  5. Build command 填 npm run build
  6. Output directory 填 dist
  7. 如果 Cloudflare 默认 Node 版本过低,在 Pages 环境变量里设置 NODE_VERSION22.12.0 或更高。
  8. 首次部署成功后再绑定自定义域名。

Cloudflare Pages 可以连接 private GitHub repo。真正需要注意的是 GitHub 授权范围:Cloudflare 必须能读取这个仓库,否则构建时拉不到源码。

自定义域名和 www 跳转

这个站点使用主域名:

https://lvxinyan.com

www.lvxinyan.com 只做 301 跳转。

这里有两个容易漏的点:

第一,DNS 记录必须让 www 请求进入 Cloudflare。否则 Redirect Rules 根本没有执行机会。

第二,如果 www 要支持 HTTPS,证书也必须覆盖 www.lvxinyan.com。最稳妥的做法是把 www.lvxinyan.com 也加到 Pages 项目的 Custom domains,等 Cloudflare 处理好证书,再用 Redirect Rules 统一跳到 apex。

规则形态是:

https://www.example.com/* -> https://example.com/${1}
http://www.example.com/* -> https://example.com/${1}

并保留 query string。

验证:

Terminal window
curl -I https://example.com
curl -I https://www.example.com
curl -I "https://www.example.com/blog/?a=1"

期望:

  • 主域名返回 200
  • www 返回 301
  • 路径和 query string 不丢。

评论系统:Giscus

主博客仓库是 private,但 Giscus 评论需要访客能读取 GitHub Discussions。因此评论数据放在一个单独的公开仓库:

lxy1992/inkstone-comments

站点配置在 src/config/site.toml

[config.comments]
enabled = true
provider = "giscus"
show_on_posts = true
[config.comments.giscus]
repo = "lxy1992/inkstone-comments"
repo_id = "R_kgDOS0-dgA"
category = "General"
category_id = "DIC_kwDOS0-dgM4C-y5q"
mapping = "pathname"
strict = "0"
reactions_enabled = "1"
emit_metadata = "0"
input_position = "top"
light_theme = "light"
dark_theme = "dark"
lang = "zh-CN"
loading = "lazy"

复刻时要做三件事:

  1. 新建一个公开 comments 仓库。
  2. 在这个仓库启用 GitHub Discussions。
  3. 安装并配置 Giscus GitHub App,然后把生成的 repo_idcategory_id 写入站点配置。

pathname 做映射的好处是迁移成本低。只要文章 URL 不变,评论就能继续按路径关联。

发布流程一:本地 Git 发布

适合在 Mac 上整理文章、批量迁移 Obsidian 内容、改主题或改站点结构。

Terminal window
npm install
npm run dev

写完后先本地验证:

Terminal window
npm test
npm run build
npm run preview

发布:

Terminal window
git status
git add <files>
git commit -m "publish blog update"
git push origin main

推送到 main 后,Cloudflare Pages 自动构建并发布。构建完成后,再验证线上:

Terminal window
curl -I https://lvxinyan.com
curl -I https://lvxinyan.com/blog/
curl -I https://lvxinyan.com/rss.xml
curl -I https://lvxinyan.com/admin/

如果是新文章,再打开文章 URL 验证页面内容和图片。

发布流程二:Sveltia CMS 发布

适合在浏览器、iPad 或临时设备上写文章。

入口:

https://lvxinyan.com/admin/

流程:

  1. 打开 /admin/
  2. 选择 Sign in with Token
  3. 粘贴只授权博客仓库的 GitHub fine-grained token。
  4. 进入 Blog collection。
  5. 新建或编辑文章。
  6. 上传图片到 public/uploads
  7. 保存。
  8. 到 GitHub 仓库确认 CMS commit。
  9. 等 Cloudflare Pages 自动构建。
  10. 验证线上 URL。

CMS 发布和本地 Git 发布最终走的是同一条链路:

写入 GitHub main -> Cloudflare Pages build -> dist -> lvxinyan.com

区别只是写 Git 的界面不同。

发布后自动清理 Cloudflare 缓存

Cloudflare Pages 部署完成后,浏览器或边缘节点有时会继续看到旧内容。这个站点用 GitHub Actions 做发布后缓存清理。

workflow 触发方式:

on:
push:
branches:
- main
deployment_status:
workflow_dispatch:

实际行为:

  1. pushmain 后等待 180 秒,给 Cloudflare Pages 留出构建时间。
  2. 调用 Cloudflare Cache Purge API。
  3. 先执行一次 purge_everything
  4. 再按本次提交中变化的 blog / gallery Markdown 生成精确 URL 清理。
  5. 手动触发时可以额外输入一个 purge_url

需要在 GitHub repository secrets 里配置:

CF_ZONE_ID
CF_API_TOKEN

CF_API_TOKEN 不要用 Global API Key。给它最小权限即可:

Zone: lvxinyan.com
Permission: Cache Purge

注意:这个 workflow 是发布后的刷新辅助,不应该承担“文章是否公开”的最终语义。draft 是否可见,仍然由 Markdown frontmatter、Astro 构建和 _redirects 共同决定。

发布前校验

这个项目把一些容易出错的事写进了 scripts/validate-site.mjs。它会检查:

  • 必要文件是否存在。
  • CMS 是否仍指向正确仓库。
  • CMS 是否使用 OAuth Worker,而不是只剩 token 登录。
  • heroImageshowHeroImage 等字段是否暴露给 CMS。
  • Giscus 配置是否存在。
  • Gallery collection 是否完整。
  • Cloudflare cache purge workflow 是否存在。
  • draft 文章是否都有对应 _redirects
  • Markdown 里的本地图片路径是否真实存在。
  • 文章有正文图片时是否有 heroImage 或显式关闭 hero 展示。

这类脚本看起来琐碎,但对个人博客很有价值。因为个人项目最容易坏在“我以为我记得”的地方:图片路径、draft 状态、缓存、CMS 字段、评论仓库权限。

常见故障

/admin/ 能打开,但看不到文章或保存失败

先看线上 CMS 配置:

Terminal window
curl https://lvxinyan.com/admin/config.yml

确认它指向正确仓库,并且有:

auth_methods: [token]

如果配置正确,再检查 GitHub token:

  • 是否是 fine-grained token。
  • 是否授权了正确仓库。
  • 是否有 Contents: Read and write
  • token 是否过期。

Cloudflare Pages 构建失败

先在本地跑:

Terminal window
npm test
npm run build

如果本地也失败,优先修本地错误。如果只有 Cloudflare 失败,重点查:

  • Node.js 版本是否满足 >=22.12.0
  • Build command 是否仍是 npm run build
  • Output directory 是否仍是 dist
  • 是否提交了 package-lock.json
  • 是否有图片路径或 frontmatter schema 错误。

文章保存了,但首页或列表看不到

检查 frontmatter:

draft: false
date: '2026-06-04T12:00:00+08:00'
summary: ...

常见原因:

  • draft: true
  • 缺少 summarydescription
  • 日期格式无法被 Astro 解析。
  • 图片路径不是 /uploads/.../images/...
  • Cloudflare Pages 最新部署还没完成。

文章改成 draft 后旧 URL 还能访问

不要先猜 CMS 没保存。按顺序查:

Terminal window
curl -I https://lvxinyan.com/blog/<slug>/
curl -I https://lvxinyan.com/blog/<slug>
curl -I https://lvxinyan.com/blog/<slug>/index.html
curl -I https://<project>.pages.dev/blog/<slug>/

如果 Pages 默认域名已经 404,但 custom domain 仍能看到旧 HTML,多半是 Cloudflare Pages 历史资源或边缘缓存问题。此时应该确认:

  • 最新构建里的 public/_redirects 是否包含这个 slug。
  • npm run redirects:generate 是否已执行。
  • GitHub Actions cache purge 是否成功。
  • 是否存在旧的 Cloudflare Dashboard 单篇 Redirect Rule 还没清理。

不要把单篇文章的发布状态长期维护在 Cloudflare Dashboard 里。状态源应该回到 GitHub 仓库。

最小复刻清单

如果你只想搭一个类似博客,可以按这个清单走:

  1. 准备一个 GitHub 仓库,可以是 private。
  2. 创建 Astro 项目。
  3. 建立 src/content/blog 和 content collection schema。
  4. 写 blog 列表、详情、RSS。
  5. 添加 /admin/ 页面加载 Sveltia CMS。
  6. 配置 public/admin/config.yml,使用 GitHub backend 和 OAuth Worker。
  7. 在 Cloudflare Pages 连接 GitHub 仓库。
  8. 设置 Build command: npm run buildOutput directory: dist
  9. 绑定 apex 域名。
  10. www 也加到 Pages custom domain,再做 301 跳转。
  11. draft 字段,并在所有公开路由过滤。
  12. 生成 _redirects,防止历史 draft URL 继续展示。
  13. 添加 npm test / npm run build 作为发布前检查。
  14. 可选:接入 Giscus、Pagefind、Gallery、Cloudflare cache purge workflow。

做到第 10 步,你已经有一个可访问的静态博客。做到第 14 步,它才更像一个长期可维护的个人发布系统。

这套方案的边界

这不是一个完整内容平台。它没有复杂的多用户权限,没有数据库后台,没有审核流,也不适合团队级内容运营。

它适合的是个人博客:

  • 文章是 Markdown。
  • Git 是备份和版本历史。
  • Cloudflare Pages 是静态托管。
  • Sveltia CMS 是浏览器里的 Git 写作界面。
  • Giscus 把评论放到公开 Discussions 仓库。
  • 所有关键发布语义尽量回到仓库,而不是散落在 Dashboard 手工配置里。

这个边界反而是优点。只要仓库还在,文章就还在;只要 Markdown 还在,以后从 Astro 换到 Hugo、Quartz 或其他系统都不难。

参考

Comments

Quiet notes for this article.