创建博客

本页内容

EmDash 博客模板提供了一个可运行的 Astro 站点,包含文章、页面、作者、分类、标签、搜索、评论、小部件和 RSS 订阅源。本教程会创建 Cloudflare 版本,发布一篇文章,并沿着模板代码追踪这篇文章的整个流程。

前提条件

开始之前,请安装 Node.js 22.12 或更高版本,以及 pnpm。

只有在部署站点时才需要 Cloudflare 账户。在本地开发期间,模板会在你的电脑上运行本地版本的数据库和文件存储。

搭建博客

下面的命令会基于 Cloudflare 博客模板创建 my-blog,并使用 pnpm 安装其依赖:

npm create emdash@latest my-blog -- --template cloudflare:blog --pm pnpm --yes

脚手架还会创建一个本地 .env 文件,其中包含 EMDASH_ENCRYPTION_KEY。生成的 .gitignore 会将 .env 排除在版本控制之外。如果命令报告依赖安装失败,请进入项目目录并运行 pnpm install,然后再继续。

启动本地开发服务器:

cd my-blog
pnpm dev

打开终端中打印的本地 URL,然后打开 /_emdash/admin。如果是首次运行,请完成设置界面。模板的种子数据会在设置期间创建内容模型和示例内容。

理解内容模型

模板在 seed/seed.json 中定义了两个集合:

  • posts 通过 supports 启用草稿、修订、搜索和 SEO,并通过 commentsEnabled: true 单独启用评论;
  • pages 支持草稿、修订和搜索。

每篇文章都有以下自定义字段:

字段用途
title必填的文章标题
featured_image随文章显示的可选图片
contentPortable Text 正文
excerpt用于文章列表和元数据回退的简短文本

EmDash 会添加系统字段,例如稳定的内容 ID、slug、状态、创建和更新时间以及发布时间。模板还为文章定义了 category 和 tag 分类法,以及可以署名一位或多位作者的 byline。

开发服务器会根据这个 schema 生成 emdash-env.d.ts。因此,getEmDashCollection("posts") 返回的条目,其 data 属性的类型为 Post。

发布第一篇文章

  1. 在管理后台侧边栏中选择 Posts,然后选择 Add New。

  2. 输入标题。EmDash 会根据标题建议一个 slug;如果公开 URL 需要不同的值,请自行编辑。

  3. 添加摘要,并在 Content 编辑器中撰写正文。

  4. 从媒体库中选择一张特色图片,或上传一张。添加替代文本,描述这张图片在文章中的作用。

  5. 在设置面板中指定 byline、分类以及相关标签。

  6. 选择 Save。该条目会成为草稿,编辑器会打开它的永久条目 URL。

  7. 选择 Preview 并检查文章页面。草稿准备就绪后,返回编辑器并选择 Publish。

在本地站点上打开 /posts/your-post-slug。这篇文章也会出现在首页和文章归档中。如果没有出现,请确认编辑器显示的是 Published,而不是 Draft 或 Scheduled。

发布之后,编辑内容会自动保存为新的草稿,而当前文章仍保持线上状态。当修订后的草稿应当取代它时,请选择 Publish changes。内容创作指南介绍了预览、定时发布、修订和编辑锁。

追踪集合查询

首页和文章归档在服务端渲染期间会调用 getEmDashCollection()。模板在数据库中按存储的 published_at 字段对文章排序:

---
import { getEmDashCollection, getTermsForEntries } from "emdash";

const { entries: posts, cacheHint } = await getEmDashCollection("posts", {
  orderBy: { published_at: "desc" },
});

if (Astro.cache?.enabled) Astro.cache.set(cacheHint);

const tagsByEntry = await getTermsForEntries(
  "posts",
  posts.map((post) => post.data.id),
  "tag",
);
---

集合查询默认返回已发布的条目。它使用数据库字段名 published_at 来排序。返回的 publishedAt 属性是供渲染使用的 JavaScript Date。

分类法辅助函数接收的是 post.data.id,因为分类法的指派属于稳定的内容 ID。链接则使用 post.id,因为那是内容加载器生成的、面向 URL 的 slug:

<a href={`/posts/${post.id}`}>
  <h2>{post.data.title}</h2>
  {post.data.excerpt && <p>{post.data.excerpt}</p>}
</a>

当前的模板使用 getTermsForEntries() 批量查询标签,而不是为每篇文章各查询一次。byline 已经由集合查询包含在 post.data.bylines 中。

追踪文章查询

动态文章路由从 URL 中读取 slug 并调用 getEmDashEntry()。下面的摘录展示了核心的查询和渲染路径,而完整的模板还会处理 SEO、byline、评论、相关文章和小部件:

---
import { decodeSlug, getEmDashEntry } from "emdash";
import { Image, PortableText } from "emdash/ui";

const slug = decodeSlug(Astro.params.slug);
if (!slug) return Astro.rewrite("/404");

const { entry: post, error, cacheHint } = await getEmDashEntry("posts", slug);

if (error) return new Response("Unable to load post", { status: 500 });
if (!post) return Astro.rewrite("/404");
if (Astro.cache?.enabled) Astro.cache.set(cacheHint);
---

<article>
  {post.data.featured_image && <Image image={post.data.featured_image} priority />}
  <h1>{post.data.title}</h1>
  <PortableText value={post.data.content} />
</article>

Image 读取编辑者选择的媒体值并生成响应式输出。PortableText 把存储的块数据转换为标题、段落、链接、图片、代码块以及其他受支持的块类型。

两个博客模板都在 astro.config.mjs 中设置了 output: "server"。这些查询在渲染请求时运行,因此已发布的内容不依赖于构建期间生成的静态路由列表。

使用分类和标签

模板为每个分类和标签都包含了归档路由。分类路由会先解析术语 slug,然后按该分类法过滤文章:

---
import { decodeSlug, getEmDashCollection, getTerm } from "emdash";

const slug = decodeSlug(Astro.params.slug);
const category = slug
  ? await getTerm("category", slug, { includeCounts: false })
  : null;

if (!category) return Astro.rewrite("/404");

const { entries: posts, error } = await getEmDashCollection("posts", {
  where: { category: category.slug },
  orderBy: { published_at: "desc" },
});

if (error) return new Response("Unable to load posts", { status: 500 });
---

tag 路由使用相同的模式,只是换成 getTerm("tag", slug) 和 where: { tag: term.slug }。编辑者在管理后台中管理术语及其指派;分类法指南涵盖了层级分类、扁平标签和自定义分类法。

getEmDashEntry() 会包含文章已指派的术语,因此详情路由无需再次查询就能渲染它们:

---
const categories = post.data.terms?.category ?? [];
const tags = post.data.terms?.tag ?? [];
---

{categories.map((category) => (
  <a href={`/category/${category.slug}`}>{category.label}</a>
))}
{tags.map((tag) => (
  <a href={`/tag/${tag.slug}`}>{tag.label}</a>
))}

添加归档分页

模板的文章归档会渲染每一篇已发布的文章。当归档变大时,请添加 limit:对 /posts/page/2 这样的编号路由使用偏移分页,对 Older posts 链接使用游标分页。在各页之间保持 orderBy: { published_at: "desc" } 不变,这样条目的顺序才不会意外改变。

分页示例展示了这两种方式,并说明了何时选择哪一种。

检查 RSS 订阅

模板已经提供了 /rss.xml。它的端点用 getEmDashCollection() 读取最新的 20 篇文章,格式化每个发布日期,并在把标题和摘要插入 XML 之前对其进行转义。它还会从 EmDash 设置中读取站点标题和标语。

发布测试文章之后,打开 /rss.xml 并搜索它的标题。如果站点要在订阅源中使用生产环境的绝对 URL,请在部署之前设置 Astro 的 site 选项;在本地开发期间,该端点会回退到当前请求的源。

至此,这个博客已经具备了创作工作流、运行时文章页面、分类法归档、媒体渲染和订阅源。接下来可以阅读查询内容了解过滤与分页,或阅读媒体库了解资源编辑与使用情况追踪。若要借助 AI 助手起草或编辑文章,请参阅 AI 工具。