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 的 Markdown | Portable Text(結構化 JSON) |
| 更新 | 需要重新建置 | 即時(SSR) |
| Schema | content.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 });
}
---