Skip to main content

emdash-cms

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

Datos de origen

Repositorio
reason-machines/trending-skills
Última actividad en el origen
2 de abril de 2026 a las 09:28
Idioma detectado de SKILL.md
inglés
Estrellas
82
Forks
15

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
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" },
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub