跳转到内容

EmDash 面向 Astro 开发者

EmDash 是一个专为 Astro 构建的 CMS——它不是一个带有 Astro 适配器的通用无头 CMS。它通过数据库支持的内容、精美的管理界面以及 WordPress 风格的功能(菜单、小部件、分类法)来扩展你的 Astro 站点,同时保留你所期望的开发者体验。

你所了解的关于 Astro 的一切仍然适用。EmDash 增强你的站点;它不会取代你的工作流程。

EmDash 提供了基于文件的 Astro 站点所缺乏的内容管理功能:

功能描述
管理界面位于 /_emdash/admin 的完整所见即所得编辑界面
数据库存储内容存储在 SQLite、libSQL 或 Cloudflare D1 中
媒体库上传、组织并提供图片和文件
导航菜单支持嵌套的拖放菜单管理
小部件区域动态侧边栏和页脚区域
站点设置全局配置(标题、徽标、社交链接)
分类法分类、标签和自定义分类法
预览系统用于草稿内容的签名预览 URL
修订版本内容版本历史

Astro 的 astro:content 集合是基于文件的,并在构建时解析。EmDash 集合是基于数据库的,并在运行时解析。

Astro 集合EmDash 集合
存储src/content/ 中的 Markdown/MDX 文件SQLite/D1 数据库
编辑代码编辑器管理界面
内容格式带有 frontmatter 的 MarkdownPortable Text(结构化 JSON)
更新需要重新构建即时(SSR)
模式content.config.ts 中的 Zod在管理界面定义,存储在数据库中
最适合开发者管理的内容编辑者管理的内容

Astro 集合和 EmDash 可以共存。使用 Astro 集合处理开发者内容(文档、更新日志),使用 EmDash 处理编辑者内容(博客文章、页面):

src/pages/index.astro
---
import { getCollection } from "astro:content";
import { getEmDashCollection } from "emdash";
// Developer-managed docs from files
const docs = await getCollection("docs");
// Editor-managed posts from database
const { entries: posts } = await getEmDashCollection("posts", {
status: "published",
limit: 5,
});
---

EmDash 需要两个配置文件。

astro.config.mjs
import { defineConfig } from "astro/config";
import emdash, { local } from "emdash/astro";
import { sqlite } from "emdash/db";
export default defineConfig({
output: "server", // Required for EmDash
integrations: [
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
}),
],
});
src/live.config.ts
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({
loader: emdashLoader(),
}),
};

这将 EmDash 注册为实时内容源。_emdash 集合在内部路由到你的内容类型(文章、页面、产品)。

EmDash 提供了遵循 Astro 实时内容集合 模式的查询函数,返回 { entries, error } 或 { entry, error }:

import { getEmDashCollection, getEmDashEntry } from "emdash";
// Get all published posts - returns { entries, error }
const { entries: posts } = await getEmDashCollection("posts", {
status: "published",
});
// Get a single post by slug - returns { entry, error, isPreview }
const { entry: post } = await getEmDashEntry("posts", "my-post");

getEmDashCollection 支持 Astro 的 getCollection 所不具备的过滤功能:

const { entries: posts } = await getEmDashCollection("posts", {
status: "published", // draft | published | archived
limit: 10, // max results
where: { category: "news" }, // taxonomy filter
});

EmDash 将富文本存储为 Portable Text,这是一种结构化的 JSON 格式。使用 PortableText 组件来渲染它:

src/pages/posts/[slug].astro
---
import { getEmDashEntry } from "emdash";
import { PortableText } from "emdash/ui";
const { slug } = Astro.params;
const { entry: post } = await getEmDashEntry("posts", slug);
if (!post) {
return Astro.redirect("/404");
}
---
<article>
<h1>{post.data.title}</h1>
<PortableText value={post.data.content} />
</article>

EmDash 提供了 Astro 内容层中不存在的 WordPress 风格功能的 API。

src/layouts/Base.astro
---
import { getMenu } from "emdash";
const primaryMenu = await getMenu("primary");
---
{primaryMenu && (
<nav>
<ul>
{primaryMenu.items.map(item => (
<li>
<a href={item.url}>{item.label}</a>
{item.children.length > 0 && (
<ul>
{item.children.map(child => (
<li><a href={child.url}>{child.label}</a></li>
))}
</ul>
)}
</li>
))}
</ul>
</nav>
)}
src/layouts/BlogPost.astro
---
import { getWidgetArea } from "emdash";
import { PortableText } from "emdash/ui";
const sidebar = await getWidgetArea("sidebar");
---
{sidebar && sidebar.widgets.length > 0 && (
<aside>
{sidebar.widgets.map(widget => (
<div class="widget">
{widget.title && <h3>{widget.title}</h3>}
{widget.type === "content" && widget.content && (
<PortableText value={widget.content} />
)}
</div>
))}
</aside>
)}
src/components/Header.astro
---
import { getSiteSettings, getSiteSetting } from "emdash";
const settings = await getSiteSettings();
// Or fetch individual values:
const title = await getSiteSetting("title");
---
<header>
{settings.logo ? (
<img src={settings.logo.url} alt={settings.title} />
) : (
<span>{settings.title}</span>
)}
{settings.tagline && <p>{settings.tagline}</p>}
</header>

通过添加钩子、存储、设置和管理界面的插件来扩展 EmDash:

astro.config.mjs
import emdash from "emdash/astro";
import seoPlugin from "@emdash-cms/plugin-seo";
export default defineConfig({
integrations: [
emdash({
// ...
plugins: [seoPlugin({ generateSitemap: true })],
}),
],
});

使用 definePlugin 创建自定义插件:

src/plugins/analytics.ts
import { definePlugin } from "emdash";
export default definePlugin({
id: "analytics",
version: "1.0.0",
capabilities: ["read:content"],
hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved", { id: event.content.id });
},
},
admin: {
settingsSchema: {
trackingId: { type: "string", label: "Tracking ID" },
},
},
});

EmDash 站点在 SSR 模式下运行。内容更改会立即生效,无需重新构建。

对于使用 getStaticPaths 的静态页面,内容在构建时获取:

src/pages/posts/[slug].astro
---
import { getEmDashCollection, getEmDashEntry } from "emdash";
export async function getStaticPaths() {
const { entries: posts } = await getEmDashCollection("posts", {
status: "published",
});
return posts.map((post) => ({
params: { slug: post.data.slug },
}));
}
const { slug } = Astro.params;
const { entry: post } = await getEmDashEntry("posts", slug);
---

对于动态页面,设置 prerender = false 以在每个请求时获取内容:

src/pages/posts/[slug].astro
---
export const prerender = false;
import { getEmDashEntry } from "emdash";
const { slug } = Astro.params;
const { entry: post, error } = await getEmDashEntry("posts", slug);
if (error) {
return new Response("Server error", { status: 500 });
}
if (!post) {
return new Response(null, { status: 404 });
}
---