EmDash 为 Astro 站点添加了管理应用、由数据库支持的集合、媒体、菜单、分类法、设置、修订和预览。页面和组件仍然是普通的 Astro 文件。
EmDash 添加了什么
| 功能 | 提供的能力 |
|---|---|
| 管理后台 | 在 /_emdash/admin 通过浏览器管理集合、媒体、菜单、分类法和设置 |
| 数据库集合 | 在请求时查询的、由编辑者管理的内容 |
| 媒体库 | 已存储的图片和文件,并为模板提供媒体字段值 |
| 草稿、修订和预览 | 发布之前的编辑工作 |
| 菜单和小部件区域 | 位于条目字段之外的、有序且可编辑的站点区域 |
| 站点设置 | 共享的标识和显示值,例如标题、标语、徽标和分页大小 |
| 插件 | 钩子、路由、存储和可选的管理后台扩展 |
这些功能与 Astro 并存,而不是取代它。路由、布局、渲染、样式和部署适配器仍然由 Astro 控制。
EmDash 和 Astro 集合
Astro 内容集合和 EmDash 集合可以共存。仓库所有的内容使用 Astro 集合,通过 /_emdash/admin 管理的内容使用 EmDash。
| Astro 内容集合 | EmDash 集合 | |
|---|---|---|
| 存储 | 项目中的文件 | SQL 数据库 |
| 编辑 | 仓库工作流 | EmDash 管理后台 |
| 查询 | getCollection() | getEmDashCollection() |
| 富文本 | Markdown 或 MDX | Portable Text |
| 交付 | 构建时或实时加载器 | 运行时实时加载器 |
当归属不同时,可以同时使用这两个集合系统。例如,产品站点可以把开发者撰写的发布说明保存在 Astro 内容集合中,把编辑者撰写的文章保存在 EmDash 中:
---
import { getCollection } from "astro:content";
import { getEmDashCollection } from "emdash";
const [releaseNotes, { entries: articles }] = await Promise.all([
getCollection("releases"),
getEmDashCollection("articles", { limit: 3 }),
]);
---
这两个结果保持独立。EmDash 不会把基于文件的条目复制到它的数据库中。
配置站点
当前的 Node 模板会把 Astro 配置为服务端输出,添加 EmDash 集成,并使用 SQLite 和本地存储适配器。
下面这份精简的配置包含了这些必需的部分:
import node from "@astrojs/node";
import react from "@astrojs/react";
import { defineConfig } from "astro/config";
import emdash, { local } from "emdash/astro";
import { sqlite } from "emdash/db";
export default defineConfig({
output: "server",
adapter: node({ mode: "standalone" }),
integrations: [
react(),
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
}),
],
});
EmDash 还提供了为 D1 和 R2 配置的 Cloudflare 模板。请从与部署目标对应的模板开始,而不要手工转换 Node 适配器。
注册实时集合
模板通过一个名为 _emdash 的 Astro 实时集合来公开 EmDash 内容:
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({ loader: emdashLoader() }),
};
getEmDashCollection() 和 getEmDashEntry() 通过这个加载器来选择所请求的内容类型。
查询集合
下面的查询读取最近发布的文章。orderBy 使用已存储的字段名,并把每个名称映射为 "asc" 或 "desc":
---
import { getEmDashCollection } from "emdash";
const { entries: posts, error, cacheHint } = await getEmDashCollection("posts", {
orderBy: { published_at: "desc" },
limit: 10,
});
if (error) return new Response("Could not load posts", { status: 500 });
if (Astro.cache?.enabled) Astro.cache.set(cacheHint);
---
{posts.map((post) => (
<article>
<h2><a href={`/posts/${post.id}`}>{post.data.title}</a></h2>
</article>
))}
匿名查询返回已发布的内容。显式的 status 过滤器适用于已认证或支持预览的代码。where 接受内容字段和分类法名称;完整的过滤器和分页结构请参阅查询内容。
查询单个条目
把 slug 或数据库 ID 传给 getEmDashEntry()。下面的路由使用其 URL slug:
---
import { decodeSlug, getEmDashEntry } from "emdash";
import { 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("Could not load the post", { status: 500 });
if (!post) return Astro.rewrite("/404");
if (Astro.cache?.enabled) Astro.cache.set(cacheHint);
---
<article>
<h1>{post.data.title}</h1>
<PortableText value={post.data.content} />
</article>
返回的 entry.id 是 Astro 的路由标识符,通常就是 slug。数据库中的内容 ID 是 entry.data.id。对于需要已存储内容 ID 的辅助函数,请使用 data.id。
使用动态 CMS 功能
EmDash 导出了一些服务端辅助函数,用于不属于单个集合条目的数据:
---
import { getMenu, getSiteSettings } from "emdash";
import { WidgetArea } from "emdash/ui";
const [menu, settings] = await Promise.all([
getMenu("primary"),
getSiteSettings(),
]);
---
<header>
<a href="/">{settings.title}</a>
<nav>
{menu?.items.map((item) => <a href={item.url}>{item.label}</a>)}
</nav>
</header>
<main><slot /></main>
<aside><WidgetArea name="sidebar" /></aside>
选择插件格式
沙箱插件和原生插件的包结构不同。沙箱插件使用 emdash-plugin.jsonc 加上默认导出的 src/plugin.ts 对象。原生插件导出一个描述符工厂,以及使用 definePlugin() 构建的 createPlugin()。
在添加插件之前,请阅读选择插件格式。不要把原生的 definePlugin() 示例复制到沙箱包中。