Astro 開發者的 EmDash

本頁內容

EmDash 是專為 Astro 建置的 CMS。它透過資料庫支援的內容、精緻的管理介面和 WordPress 風格的功能(選單、小工具、分類法)擴充您的 Astro 網站,同時保留您期望的開發者體驗。

您對 Astro 的所有了解仍然適用。EmDash 在您現有的 Astro 工作流程之上新增內容管理。

EmDash 新增了什麼

EmDash 提供了基於檔案的 Astro 網站所缺少的內容管理功能:

功能描述
管理介面/_emdash/admin 的完整 WYSIWYG 編輯介面
資料庫儲存內容儲存在 SQLite、libSQL、Cloudflare D1 或 PostgreSQL 中
媒體庫上傳、組織和提供圖片和檔案
導覽選單具有巢狀功能的拖放選單管理
小工具區域動態側邊欄和頁尾區域
網站設定全域配置(標題、標誌、社交連結)
分類法類別、標籤和自訂分類法
預覽系統草稿內容的簽章預覽 URL
修訂版本內容版本歷史記錄

Astro 集合 vs EmDash

Astro 的 astro:content 集合基於檔案,在建置時解析。EmDash 集合基於資料庫,在執行階段解析。

Astro 集合EmDash 集合
儲存src/content/ 中的 Markdown/MDX 檔案SQL 資料庫(SQLite、libSQL、D1 或 Postgres)
編輯程式碼編輯器管理介面
內容格式帶有 frontmatter 的 MarkdownPortable Text(結構化 JSON)
更新需要重新建置即時(SSR)
架構content.config.ts 中的 Zod在管理後台定義,儲存在資料庫中
最適合開發者管理的內容編輯者管理的內容

一起使用

Astro 集合和 EmDash 可以共存。將 Astro 集合用於開發者內容(文件、變更記錄),將 EmDash 用於編輯者內容(部落格文章、頁面):

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

以下配置在伺服器輸出模式下將 EmDash 註冊為 Astro 整合:

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",
			}),
		}),
	],
});

即時集合載入器

以下檔案將 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";

// 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");

Astro

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

// Get all blog entries
const posts = await getCollection("blog");

// Get a single entry by slug
const post = await getEntry("blog", "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 元件渲染它:

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

網站設定

以下元件讀取全域網站設定並渲染標誌或標題:

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

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

export default defineConfig({
	integrations: [
		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: "Tracking 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 });
}
---

下一步