Communitygithub.com

reason-machines/trending-skills

EmDash(Astro と Cloudflare 上に構築されたフルスタック TypeScript CMS)を利用するための AI コーディングエージェントスキルです。

trending-skills とは?

このスキルは、AI エージェントが EmDash CMS プロジェクトを効率的に操作するための包括的なガイドを提供します。EmDash は Astro と Cloudflare 上に構築されたフルスタック TypeScript CMS であり、WordPress の精神的な後継者として設計されています。プラグインはサンドボックス化された Worker 分離環境で実行され、安全で拡張性の高いアーキテクチャを実現しています。このスキルでは、プロジェクトのスキャフォールディング、CLI コマンド、Cloudflare D1/R2/KV を使用した設定、Portable Text を使用したリッチテキストのレンダリング、プラグイン開発、認証、WordPress からの移行、MCP サーバー統合など、EmDash の主要機能をカバーしています。AI アシスタントは、このドキュメントを参照して、コード生成、設定支援、トラブルシューティングを正確に行うことができます。

対応✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/reason-machines/trending-skills/tree/HEAD/skills/emdash-cms

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

プレビュー

スキル README より

trending-skills preview

ドキュメント

EmDash CMS

ara.so によるスキル — Daily 2026 Skills コレクション。

EmDash は、Astro と Cloudflare 上に構築されたフルスタック TypeScript CMS です。WordPress の精神的な後継者として、拡張性が高く、開発者にとって使いやすく、ファイルシステムやデータベースへのフルアクセスではなく、サンドボックス化された Worker 分離環境でプラグインを実行するプラグインシステムを採用しています。EmDash はリッチテキストを HTML ではなく Portable Text (構造化 JSON) として保存し、passkey ファーストの認証をサポートし、Cloudflare (D1 + R2 + Workers) または任意の Node.js サーバー + SQLite 上で動作します。


インストール

新しいプロジェクトをスキャフォールディングする

npm create emdash@latest

プロンプトに従って、テンプレート (blog、marketing、portfolio、starter、blank) とプラットフォーム (Cloudflare または Node.js/SQLite) を選択します。

Cloudflare に直接デプロイする

README のワンクリックデプロイボタンを使用するか、以下を実行します:

npm create emdash@latest -- --template blog-cloudflare
cd my-site
npm run deploy

既存の Astro プロジェクトに EmDash を追加する

npm install emdash
// astro.config.mjs
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { d1 } from "emdash/db";

export default defineConfig({
  integrations: [
    emdash({
      database: d1(), // Cloudflare D1
    }),
  ],
});

Node.js + SQLite (Cloudflare アカウント不要) の場合:

// astro.config.mjs
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { sqlite } from "emdash/db";

export default defineConfig({
  integrations: [
    emdash({
      database: sqlite({ path: "./content.db" }),
    }),
  ],
});

主要な CLI コマンド

# 新しい EmDash プロジェクトをスキャフォールディング
npm create emdash@latest

# ライブスキーマから TypeScript の型を生成
npx emdash types

# サンプルコンテンツでデモサイトをシード
npx emdash seed

# データベースマイグレーションを実行
npx emdash migrate

# 開発サーバーを起動 (標準の Astro コマンド)
npx astro dev

# 本番用にビルド
npx astro build

# 管理パネルを開く (開発サーバー起動後)
open http://localhost:4321/_emdash/admin

モノレポ / コントリビューター向けコマンド

pnpm install
pnpm build
pnpm test          # すべてのテストを実行
pnpm typecheck     # TypeScript チェック
pnpm lint:quick    # 高速リント (1 秒未満)
pnpm format        # oxfmt でフォーマット

# デモを実行 (Node.js + SQLite、Cloudflare 不要)
pnpm --filter emdash-demo seed
pnpm --filter emdash-demo dev

設定

Cloudflare (D1 + R2 + KV + Worker ローダー)

// wrangler.jsonc
{
  "name": "my-emdash-site",
  "compatibility_date": "2025-01-01",
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "emdash-content",
      "database_id": "$DATABASE_ID"
    }
  ],
  "r2_buckets": [
    {
      "binding": "MEDIA",
      "bucket_name": "emdash-media"
    }
  ],
  "kv_namespaces": [
    {
      "binding": "SESSIONS",
      "id": "$KV_NAMESPACE_ID"
    }
  ],
  // このブロックを削除すると、サンドボックスプラグインが無効になります (無料アカウント)
  "worker_loaders": [
    {
      "binding": "PLUGIN_LOADER"
    }
  ]
}
// astro.config.mjs
import emdash from "emdash/astro";
import { d1 } from "emdash/db";
import { r2 } from "emdash/storage";
import { kv } from "emdash/sessions";

export default defineConfig({
  integrations: [
    emdash({
      database: d1({ binding: "DB" }),
      storage: r2({ binding: "MEDIA" }),
      sessions: kv({ binding: "SESSIONS" }),
    }),
  ],
});

Node.js + SQLite

// astro.config.mjs
import emdash from "emdash/astro";
import { sqlite } from "emdash/db";
import { localFiles } from "emdash/storage";

export default defineConfig({
  integrations: [
    emdash({
      database: sqlite({ path: "./content.db" }),
      storage: localFiles({ dir: "./public/uploads" }),
    }),
  ],
});

PostgreSQL / Turso

import { postgres } from "emdash/db";
import { turso } from "emdash/db";

// PostgreSQL
database: postgres({ url: process.env.DATABASE_URL })

// Turso/libSQL
database: turso({
  url: process.env.TURSO_DATABASE_URL,
  authToken: process.env.TURSO_AUTH_TOKEN,
})

コンテンツのクエリ

コンテンツタイプは管理 UI で定義されます (コードは不要)。コレクションを作成した後、型を生成します:

npx emdash types

これにより、src/emdash.d.ts に型定義が書き込まれます。

Astro ページでコレクションを取得する

---
// src/pages/blog/index.astro
import { getEmDashCollection } from "emdash";

const { entries: posts } = await getEmDashCollection("posts", {
  filter: { status: "published" },
  sort: { field: "publishedAt", direction: "desc" },
  limit: 10,
});
---

<ul>
  {posts.map((post) => (
    <li>
      <a href={`/blog/${post.data.slug}`}>{post.data.title}</a>
      <time datetime={post.data.publishedAt}>{post.data.publishedAt}</time>
    </li>
  ))}
</ul>

単一のエントリを取得する

---
// src/pages/blog/[slug].astro
import { getEmDashEntry, renderPortableText } from "emdash";

const { slug } = Astro.params;
const post = await getEmDashEntry("posts", { slug });

if (!post) return Astro.redirect("/404");

const { Content } = await renderPortableText(post.data.body);
---

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

ページネーション

---
import { getEmDashCollection } from "emdash";

const page = Number(Astro.params.page ?? 1);
const { entries: posts, total } = await getEmDashCollection("posts", {
  filter: { status: "published" },
  sort: { field: "publishedAt", direction: "desc" },
  limit: 10,
  offset: (page - 1) * 10,
});
---

タクソノミーによるフィルタリング

---
import { getEmDashCollection } from "emdash";

const { entries: posts } = await getEmDashCollection("posts", {
  filter: {
    status: "published",
    tags: { contains: "typescript" },
  },
});
---

Portable Text のレンダリング

EmDash はリッチテキストを HTML ではなく Portable Text (構造化 JSON) として保存します。

---
import { renderPortableText } from "emdash";

const post = await getEmDashEntry("posts", { slug: Astro.params.slug });
const { Content } = await renderPortableText(post.data.body);
---

<Content />

カスタムブロックレンダラー

// src/portable-text.ts
import { definePortableTextComponents } from "emdash/blocks";

export const components = definePortableTextComponents({
  types: {
    callout: ({ value }) => `
      <div class="callout callout--${value.type}">
        ${value.text}
      </div>
    `,
    image: ({ value }) => `
      <figure>
        <img src="${value.url}" alt="${value.alt ?? ""}" />
        ${value.caption ? `<figcaption>${value.caption}</figcaption>` : ""}
      </figure>
    `,
  },
  marks: {
    highlight: ({ children }) => `<mark>${children}</mark>`,
  },
});
---
import { renderPortableText } from "emdash";
import { components } from "../portable-text";

const { Content } = await renderPortableText(post.data.body, { components });
---
<Content />

プラグイン開発

プラグインは主要な拡張メカニズムです。Cloudflare 上では、宣言された機能マニフェストを持つサンドボックス化された Worker 分離環境で実行されます。Node.js 上では、インプロセス (セーフモード) で実行されます。

最小限のプラグイン

// plugins/my-plugin/index.ts
import { definePlugin } from "emdash/plugin";

export default () =>
  definePlugin({
    id: "my-plugin",
    name: "My Plugin",
    version: "1.0.0",
    capabilities: [],
    hooks: {},
  });

コンテンツフックとメール機能を持つプラグイン

import { definePlugin } from "emdash/plugin";

export default () =>
  definePlugin({
    id: "notify-on-publish",
    name: "Notify on Publish",
    version: "1.0.0",
    capabilities: ["read:content", "email:send"],
    hooks: {
      "content:afterSave": async (event, ctx) => {
        if (event.content.status !== "published") return;

        await ctx.email.send({
          to: "[email protected]",
          subject: `New post published: ${event.content.title}`,
          text: `"${event.content.title}" is now live.`,
        });
      },
    },
  });

KV ストレージと管理設定を持つプラグイン

import { definePlugin } from "emdash/plugin";

export default () =>
  definePlugin({
    id: "view-counter",
    name: "View Counter",
    version: "1.0.0",
    capabilities: ["read:content", "kv:read", "kv:write"],

    settings: {
      schema: {
        resetDaily: { type: "boolean", default: false, label: "Reset counts daily" },
      },
    },

    hooks: {
      "content:beforeRender": async (event, ctx) => {
        const key = `views:${event.content.id}`;
        const current = Number(await ctx.kv.get(key) ?? 0);
        await ctx.kv.set(key, String(current + 1));
        event.content.meta.views = current + 1;
      },
    },

    adminPages: [
      {
        path: "/analytics",
        title: "View Analytics",
        component: "ViewAnalytics",
      },
    ],
  });

カスタムブロックタイプを持つプラグイン

import { definePlugin } from "emdash/plugin";
import { defineBlock } from "emdash/blocks";

export default () =>
  definePlugin({
    id: "callout-block",
    name: "Callout Block",
    version: "1.0.0",
    capabilities: [],

    blocks: [
      defineBlock({
        name: "callout",
        title: "Callout",
        fields: [
          { name: "type", type: "select", options: ["info", "warning", "danger"], default: "info" },
          { name: "text", type: "text", label: "Message" },
        ],
      }),
    ],

    hooks: {},
  });

カスタム API ルートを持つプラグイン

import { definePlugin } from "emdash/plugin";

export default () =>
  definePlugin({
    id: "newsletter",
    name: "Newsletter",
    version: "1.0.0",
    capabilities: ["kv:write"],

    apiRoutes: [
      {
        method: "POST",
        path: "/subscribe",
        handler: async (request, ctx) => {
          const { email } = await request.json();
          await ctx.kv.set(`subscriber:${email}`, "true");
          return new Response(JSON.stringify({ ok: true }), {
            headers: { "Content-Type": "application/json" },
          });
        },
      },
    ],

    hooks: {},
  });

利用可能な機能

機能許可される操作
read:content公開済みコンテンツの読み取り
write:contentコンテンツの作成と更新
read:usersユーザープロファイルの読み取り
email:send設定されたプロバイダー経由でメールを送信
kv:readプラグインの KV 名前空間から読み取り
kv:writeプラグインの KV 名前空間に書き込み
http:fetchアウトバウンド HTTP リクエストを行う
storage:readメディアストレージから読み取り
storage:writeメディアストレージに書き込み

利用可能なフック

// コンテンツライフサイクル
"content:beforeSave"
"content:afterSave"
"content:beforeDelete"
"content:afterDelete"
"content:beforeRender"

// 認証ライフサイクル
"auth:afterLogin"
"auth:afterLogout"
"auth:afterRegister"

// メディアライフサイクル
"media:beforeUpload"
"media:afterUpload"
"media:beforeDelete"

// 管理ライフサイクル
"admin:init"

認証

EmDash は passkey ファースト (WebAuthn) 認証を使用し、OAuth およびマジックリンクのフォールバックを備えています。

OAuth プロバイダーを設定する

// astro.config.mjs
import emdash from "emdash/astro";
import { github, google } from "emdash/auth";

export default defineConfig({
  integrations: [
    emdash({
      database: d1(),
      auth: {
        providers: [
          github({
            clientId: process.env.GITHUB_CLIENT_ID,
            clientSecret: process.env.GITHUB_CLIENT_SECRET,
          }),
          google({
            clientId: process.env.GOOGLE_CLIENT_ID,
            clientSecret: process.env.GOOGLE_CLIENT_SECRET,
          }),
        ],
      },
    }),
  ],
});

マジックリンクメールを設定する

auth: {
  providers: [
    magicLink({
      from: "[email protected]",
      // 設定されたメールアダプターを使用
    }),
  ],
}

ページを保護する

---
// src/pages/dashboard.astro
import { requireAuth } from "emdash/auth";

const user = await requireAuth(Astro);
// 認証されていない場合は /login にリダイレクト
---

<p>ようこそ、{user.name} さん!</p>

現在のユーザーを取得する (オプション)

---
import { getUser } from "emdash/auth";

const user = await getUser(Astro); // ログインしていない場合は null
---

{user ? <p>こんにちは、{user.name} さん</p> : <a href="/login">サインイン</a>}

ロール

import { requireRole } from "emdash/auth";

// API ルートまたはページ内
const user = await requireRole(Astro, "editor");
// ロール: "administrator" | "editor" | "author" | "contributor"

WordPress からの移行

WXR エクスポートファイルからインポート

npx emdash import:wordpress ./export.xml

WordPress REST API からインポート

npx emdash import:wordpress --url https://yoursite.wordpress.com --api-key $WP_API_KEY

WordPress.com からインポート

npx emdash import:wordpress --wpcom --site yoursite.wordpress.com

インポーターは、投稿、ページ、メディアの添付ファイル、カテゴリー、タグ、作成者、コメントを移行します。Gutenberg ブロックは gutenberg-to-portable-text パッケージを介して Portable Text に変換されます。


コンテンツスキーマ (管理 UI)

コンテンツタイプは、コードではなく、/_emdash/admin の管理パネルで作成されます。コレクションを作成または変更した後、TypeScript の型を再生成します:

npx emdash types
# src/emdash.d.ts に書き込まれます

スキーマビルダーで利用可能なフィールドタイプ

  • text — 短い文字列
  • richText — Portable Text (TipTap エディター)
  • number — 整数または浮動小数点数
  • boolean — true/false トグル
  • date / datetime — 日付ピッカー
  • select — 単一選択ドロップダウン
  • multiSelect — 複数選択
  • image — メディアライブラリピッカー
  • file — ファイル添付
  • relation — 別のコレクションエントリへのリンク
  • slug — URL セーフな文字列、ソースフィールドから自動生成
  • json — 生の JSON

MCP サーバー (AI ツール統合)

EmDash には、AI エージェントが EmDash プロジェクトと対話できるようにする組み込みの MCP サーバーが含まれています。npx emdash mcp を実行して起動します。詳細については、EmDash MCP ドキュメント を参照してください。


リソース

関連スキル