Skip to main content

emdash-cms

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

Aller à l'installation

Informations de source

Dépôt
reason-machines/trending-skills
Dernière activité de la source
2 avril 2026 à 09:28
Langue détectée de SKILL.md
anglais
Étoiles
82
Forks
15

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
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" },
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub