Claude
Skills
Sign in
Back

emdash-cms

Included with Lifetime
$97 forever

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

Web Dev

What this skill does


# 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: "[email protected]",
          subject: `New post published: ${event.content.title}`,
  
Files: 1
Size: 17.1 KB
Complexity: 25/100
Category: Web Dev

Related in Web Dev