コンテンツにスキップ

Astro開発者向けEmDash

EmDashは、Astro専用に構築されたCMSです。汎用ヘッドレスCMSにAstroアダプターを付けたものではありません。データベースバックエンドのコンテンツ、洗練された管理UI、WordPressスタイルの機能(メニュー、ウィジェット、タクソノミー)を提供してAstroサイトを拡張しつつ、開発者が期待する開発者体験を維持します。

Astroについて既に知っていることはすべてそのまま適用できます。EmDashはサイトを強化しますが、ワークフローを置き換えるものではありません。

EmDashは、ファイルベースのAstroサイトに欠けているコンテンツ管理機能を提供します:

機能説明
管理UI/_emdash/admin で利用可能な完全なWYSIWYG編集インターフェース
データベースストレージSQLite、libSQL、またはCloudflare D1にコンテンツを保存
メディアライブラリ画像やファイルのアップロード、整理、配信
ナビゲーションメニュードラッグ&ドロップによるネスト可能なメニュー管理
ウィジェットエリア動的なサイドバーやフッター領域
サイト設定グローバル設定(タイトル、ロゴ、ソーシャルリンク)
タクソノミーカテゴリ、タグ、カスタムタクソノミー
プレビューシステム下書きコンテンツ用の署名付きプレビューURL
リビジョンコンテンツのバージョン履歴

Astroのastro:contentコレクションはファイルベースで、ビルド時に解決されます。EmDashコレクションはデータベースバックエンドで、ランタイムに解決されます。

AstroコレクションEmDashコレクション
ストレージsrc/content/内のMarkdown/MDXファイルSQLite/D1データベース
編集コードエディター管理UI
コンテンツ形式フロントマター付き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";
// ファイルからの開発者管理ドキュメント
const docs = await getCollection("docs");
// データベースからの編集者管理投稿
const { entries: posts } = await getEmDashCollection("posts", {
status: "published",
limit: 5,
});
---

EmDashには2つの設定ファイルが必要です。

ts title="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",
}),
}),
],
});
ts title="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";
// 公開済み投稿をすべて取得 - { entries, error }を返す
const { entries: posts } = await getEmDashCollection("posts", {
status: "published",
});
// スラッグで単一投稿を取得 - { 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コンポーネントでレンダリングします:

astro title="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を提供します。

astro title="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>
)}
astro title="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>
)}
astro title="src/components/Header.astro"
---
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>

フック、ストレージ、設定、管理UIを追加するプラグインでEmDashを拡張します:

ts title="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でカスタムプラグインを作成:

ts title="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("コンテンツが保存されました", { id: event.content.id });
},
},
admin: {
settingsSchema: {
trackingId: { type: "string", label: "Tracking ID" },
},
},
});

EmDashサイトはSSRモードで実行されます。コンテンツの変更はリビルドなしですぐに反映されます。

getStaticPathsを使用する静的ページでは、コンテンツはビルド時に取得されます:

astro title="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 を設定します:

astro title="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("サーバーエラー", { status: 500 });
}
if (!post) {
return new Response(null, { status: 404 });
}
---