Astro 开发者的 EmDash 指南

本页内容

EmDash 是一个专门为 Astro 构建的 CMS。它为你的 Astro 网站扩展了数据库驱动的内容、精致的管理界面和 WordPress 风格的功能(菜单、小部件、分类法),同时保持你期望的开发者体验。

你所了解的关于 Astro 的一切仍然适用。EmDash 在你现有的 Astro 工作流之上添加了内容管理。

EmDash 增加了什么

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

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

Astro Collections 与 EmDash

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

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

两者结合使用

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

---
import { getCollection } from "astro:content";
import { getEmDashCollection } from "emdash";

// 从文件获取开发者管理的文档
const docs = await getCollection("docs");

// 从数据库获取编辑者管理的文章
const { entries: posts } = await getEmDashCollection("posts", {
  status: "published",
  limit: 5,
});
---

配置

EmDash 需要两个配置文件。

Astro 集成

以下配置在服务器输出模式下将 EmDash 注册为 Astro 集成:

import { defineConfig } from "astro/config";
import react from "@astrojs/react";
import emdash, { local } from "emdash/astro";
import { sqlite } from "emdash/db";

export default defineConfig({
	output: "server", // EmDash 必需
	integrations: [
		react(), // 必需 — 管理界面是一个 React 应用
		emdash({
			database: sqlite({ url: "file:./data.db" }),
			storage: local({
				directory: "./uploads",
				baseUrl: "/_emdash/api/media/file",
			}),
		}),
	],
});

Live Collections Loader

以下文件将 EmDash 注册为实时内容源:

import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";

export const collections = {
	_emdash: defineLiveCollection({
		loader: emdashLoader(),
	}),
};

_emdash 集合在内部路由到你的内容类型(文章、页面、产品)。

查询内容

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

EmDash

import { getEmDashCollection, getEmDashEntry } from "emdash";

// 获取所有已发布的文章 - 返回 { entries, error }
const { entries: posts } = await getEmDashCollection("posts", {
status: "published",
});

// 通过 slug 获取单篇文章 - 返回 { entry, error, isPreview }
const { entry: post } = await getEmDashEntry("posts", "my-post");

Astro

import { getCollection, getEntry } from "astro:content";

// 获取所有博客条目
const posts = await getCollection("blog");

// 通过 slug 获取单个条目
const post = await getEntry("blog", "my-post");

筛选选项

getEmDashCollection 支持 Astro 的 getCollection 所没有的筛选功能:

const { entries: posts } = await getEmDashCollection("posts", {
	status: "published", // draft | published | archived
	limit: 10, // 最大结果数
	where: { category: "news" }, // 分类法筛选
});

渲染内容

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

EmDash

---
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>

Astro

---
import { getEntry, render } from "astro:content";

const { slug } = Astro.params;
const post = await getEntry("blog", slug);
const { Content } = await render(post);

---

<article>
  <h1>{post.data.title}</h1>
  <Content />
</article>

动态功能

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

导航菜单

以下布局按位置获取菜单并渲染嵌套项:

---
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>
)}

小部件区域

以下布局获取小部件区域并渲染每个小部件:

---
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>
)}

网站设置

以下组件读取全局网站设置并渲染 Logo 或标题:

---
import { getSiteSettings, getSiteSetting } from "emdash";

const settings = await getSiteSettings();
// 或获取单个值:
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:

import react from "@astrojs/react";
import emdash from "emdash/astro";
import seoPlugin from "@emdash-cms/plugin-seo";

export default defineConfig({
	integrations: [
		react(),
		emdash({
			// ...
			plugins: [seoPlugin({ generateSitemap: true })],
		}),
	],
});

使用 definePlugin 创建自定义插件:

import { definePlugin } from "emdash";

export default definePlugin({
	id: "analytics",
	version: "1.0.0",
	capabilities: ["content:read"],

	hooks: {
		"content:afterSave": async (event, ctx) => {
			ctx.log.info("Content saved", { id: event.content.id });
		},
	},

	admin: {
		settingsSchema: {
			trackingId: { type: "string", label: "跟踪 ID" },
		},
	},
});

服务器渲染

EmDash 网站以 SSR 模式运行,因此内容在运行时提供,更改会立即生效。

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

---
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 以在每次请求时获取内容:

---
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 });
}
---

下一步