Skip to main content

emdash-cms

AI coding agent skill for building with EmDash, the full-stack TypeScript CMS built on Astro and Cloudflare

معلومات المصدر

المستودع
reason-machines/trending-skills
آخر نشاط في المصدر
٢ أبريل ٢٠٢٦ في ٠٩:٢٨
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٨٣
التفرعات
١٥

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
emdash-cms
description
AI coding agent skill for building with EmDash, the full-stack TypeScript CMS built on Astro and Cloudflare
triggers
["set up EmDash CMS","add EmDash to my Astro site","create a content type in EmDash","build an EmDash plugin","query EmDash content collections","migrate from WordPress to EmDash","configure EmDash with Cloudflare D1","add authentication to my EmDash site"]
# EmDash CMS > Skill by [ara.so](https://ara.so) — Daily 2026 Skills collection. EmDash is a full-stack TypeScript CMS built on Astro and Cloudflare. It is the spiritual successor to WordPress: extensible, developer-friendly, and powered by a plugin system that runs plugins in sandboxed Worker isolates rather than with full filesystem/database access. EmDash stores rich text as Portable Text (structured JSON) rather than HTML, supports passkey-first auth, and runs on Cloudflare (D1 + R2 + Workers) or any Node.js server with SQLite. --- ## Installation ### Scaffold a new project ```bash npm create emdash@latest ``` Follow the prompts to choose a template (blog, marketing, portfolio, starter, blank) and a platform (Cloudflare or Node.js/SQLite). ### Deploy to Cloudflare directly Use the one-click deploy button from the README, or: ```bash npm create emdash@latest -- --template blog-cloudflare cd my-site npm run deploy ``` ### Add EmDash to an existing Astro project ```bash npm install emdash ``` ```typescript // 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 }), ], }); ``` For Node.js + SQLite (no Cloudflare account needed): ```typescript // 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" }), }), ], }); ``` --- ## Key CLI Commands ```bash # Scaffold a new EmDash project npm create emdash@latest # Generate TypeScript types from your live schema npx emdash types # Seed the demo site with sample content npx emdash seed # Run database migrations npx emdash migrate # Start the dev server (standard Astro command) npx astro dev # Build for production npx astro build # Open admin panel (after dev server starts) open http://localhost:4321/_emdash/admin ``` ### Monorepo / contributor commands ```bash pnpm install pnpm build pnpm test # run all tests pnpm typecheck # TypeScript check pnpm lint:quick # fast lint (< 1s) pnpm format # format with oxfmt # Run the demo (Node.js + SQLite, no Cloudflare needed) pnpm --filter emdash-demo seed pnpm --filter emdash-demo dev ``` --- ## Configuration ### Cloudflare (D1 + R2 + KV + Worker Loaders) ```jsonc // 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" } ], // Remove this block to disable sandboxed plugins (free accounts) "worker_loaders": [ { "binding": "PLUGIN_LOADER" } ] } ``` ```typescript // 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 ```typescript // 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 ```typescript 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, }) ``` --- ## Querying Content Content types are defined in the admin UI (no code required). After creating a collection, generate types: ```bash npx emdash types ``` This writes type definitions to `src/emdash.d.ts`. ### Fetch a collection in an Astro page ```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> ``` ### Fetch a single entry ```astro --- // 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> ``` ### Pagination ```astro --- 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, }); --- ``` ### Filtering by taxonomy ```astro --- import { getEmDashCollection } from "emdash"; const { entries: posts } = await getEmDashCollection("posts", { filter: { status: "published", tags: { contains: "typescript" }, }, }); --- ``` --- ## Portable Text Rendering EmDash stores rich text as [Portable Text](https://www.portabletext.org/) (structured JSON), not HTML. ```astro --- import { renderPortableText } from "emdash"; const post = await getEmDashEntry("posts", { slug: Astro.params.slug }); const { Content } = await renderPortableText(post.data.body); --- <Content /> ``` ### Custom block renderers ```typescript // 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>`, }, }); ``` ```astro --- import { renderPortableText } from "emdash"; import { components } from "../portable-text"; const { Content } = await renderPortableText(post.data.body, { components }); --- <Content /> ``` --- ## Plugin Development Plugins are the primary extension mechanism. On Cloudflare, they run in sandboxed Worker isolates with a declared capability manifest. On Node.js, they run in-process (safe mode). ### Minimal plugin ```typescript // 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: {}, }); ``` ### Plugin with content hooks and email ```typescript 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: "editors@example.com", subject: `New post published: ${event.content.title}`, text: `"${event.content.title}" is now live.`, }); }, }, }); ``` ### Plugin with KV storage and admin settings ```typescript 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", }, ], }); ``` ### Plugin with custom block type ```typescript 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" },
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub