创建博客

本页内容

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

脚手架工具还会创建包含 EMDASH_ENCRYPTION_KEY 的本地 .env 文件。生成的 .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、状态、创建和更新时间以及发布时间。模板还为文章定义了 categorytag 分类法,以及可以标注一位或多位作者的署名行。

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

发布第一篇文章

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

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

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

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

  5. 在设置面板中分配署名行、分类和相关标签。

  6. 选择 Save。条目将变为草稿,编辑器会打开其永久条目 URL。

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

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

发布后,编辑内容会自动保存到新草稿,而当前文章保持在线。当修订后的草稿应该替换时,选择 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() 批量查询标签,而不是为每篇文章单独查询一次。署名行已经通过集合查询包含在 post.data.bylines 中。

追踪文章查询

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

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

const slug = decodeSlug(Astro.params.slug);
if (!slug) return Astro.redirect("/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.redirect("/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.redirect("/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 等编号路由使用偏移分页,或对更早的文章链接使用游标分页。不要在页面之间更改 orderBy: { published_at: "desc" },以免条目顺序意外改变。

分页示例展示了两种方法并解释了何时选择每种方法。

检查 RSS 订阅

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

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

此时博客已经拥有创作工作流、运行时文章页面、分类法归档、媒体渲染和订阅。继续阅读查询内容了解过滤和分页,或媒体库了解资产编辑和使用跟踪。要使用 AI 助手起草或编辑文章,请参阅 AI 工具