Communitygithub.com

reason-machines/trending-skills

Habilidade para agente de IA que permite construir com EmDash, um CMS TypeScript full-stack baseado em Astro e Cloudflare.

O que é trending-skills?

Esta habilidade para agente de IA foi projetada para desenvolvedores que desejam construir e gerenciar sites com EmDash, um CMS TypeScript full-stack que é o sucessor espiritual do WordPress. Ela oferece suporte a múltiplas plataformas: Cloudflare (com D1, R2 e Workers) ou Node.js com SQLite, PostgreSQL e Turso. A habilidade permite que um agente de IA escale novos projetos, adicione EmDash a projetos Astro existentes, execute comandos CLI (como seed de dados, migrações e geração de tipos), consulte conteúdo por meio da API headless da EmDash, renderize Portable Text (JSON estruturado), desenvolva plugins com capacidades declarativas (sandboxed no Cloudflare) e gerencie autenticação (passkeys, OAuth e links mágicos). Ela também inclui ferramentas para migração do WordPress, criação de esquemas de conteúdo pela UI administrativa, e integração com servidores MCP para ferramentas de IA. O agente pode usar essa habilidade para automatizar a configuração do CMS, consultar posts de blog, gerenciar coleções e até mesmo criar e implantar plugins personalizados.

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

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Prévia

Do README da skill

trending-skills preview

Documentação

EmDash CMS

Habilidade por ara.so — Coleção Daily 2026 Skills.

EmDash é um CMS TypeScript full-stack construído em Astro e Cloudflare. É o sucessor espiritual do WordPress: extensível, amigável para desenvolvedores e alimentado por um sistema de plugins que executa plugins em isolados de Worker em sandbox, em vez de com acesso completo ao sistema de arquivos/banco de dados. O EmDash armazena rich text como Portable Text (JSON estruturado) em vez de HTML, suporta autenticação baseada em passkeys e roda no Cloudflare (D1 + R2 + Workers) ou qualquer servidor Node.js com SQLite.


Instalação

Iniciar um novo projeto

npm create emdash@latest

Siga as instruções para escolher um modelo (blog, marketing, portfólio, iniciante, em branco) e uma plataforma (Cloudflare ou Node.js/SQLite).

Implantar diretamente no Cloudflare

Use o botão de implantação com um clique do README, ou:

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

Adicionar EmDash a um projeto Astro existente

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
    }),
  ],
});

Para Node.js + SQLite (sem necessidade de conta 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" }),
    }),
  ],
});

Principais Comandos CLI

# Iniciar um novo projeto EmDash
npm create emdash@latest

# Gerar tipos TypeScript a partir do esquema ativo
npx emdash types

# Semear o site de demonstração com conteúdo de exemplo
npx emdash seed

# Executar migrações de banco de dados
npx emdash migrate

# Iniciar o servidor de desenvolvimento (comando padrão Astro)
npx astro dev

# Compilar para produção
npx astro build

# Abrir o painel administrativo (após iniciar o servidor de desenvolvimento)
open http://localhost:4321/_emdash/admin

Comandos para monorepo / contribuidores

pnpm install
pnpm build
pnpm test          # executar todos os testes
pnpm typecheck     # verificação TypeScript
pnpm lint:quick    # lint rápido (< 1s)
pnpm format        # formatar com oxfmt

# Executar a demonstração (Node.js + SQLite, sem necessidade Cloudflare)
pnpm --filter emdash-demo seed
pnpm --filter emdash-demo dev

Configuração

Cloudflare (D1 + R2 + KV + Worker Loaders)

// 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"
    }
  ],
  // Remova este bloco para desabilitar plugins em sandbox (contas gratuitas)
  "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,
})

Consultando Conteúdo

Os tipos de conteúdo são definidos na UI administrativa (sem necessidade de código). Após criar uma coleção, gere os tipos:

npx emdash types

Isso escreve definições de tipo em src/emdash.d.ts.

Buscar uma coleção em uma página 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>

Buscar uma única entrada

---
// 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>

Paginação

---
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,
});
---

Filtrando por taxonomia

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

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

Renderização de Portable Text

O EmDash armazena rich text como Portable Text (JSON estruturado), não HTML.

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

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

<Content />

Renderizadores de bloco personalizados

// 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 />

Desenvolvimento de Plugins

Plugins são o mecanismo de extensão principal. No Cloudflare, eles rodam em isolados de Worker em sandbox com um manifesto de capacidades declarado. No Node.js, rodam em processo (modo seguro).

Plugin mínimo

// 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 com hooks de conteúdo e email

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: `Novo post publicado: ${event.content.title}`,
          text: `"${event.content.title}" está agora no ar.`,
        });
      },
    },
  });

Plugin com armazenamento KV e configurações administrativas

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: "Redefinir contagens diariamente" },
      },
    },

    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: "Analytics de Visualizações",
        component: "ViewAnalytics",
      },
    ],
  });

Plugin com tipo de bloco personalizado

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: "Mensagem" },
        ],
      }),
    ],

    hooks: {},
  });

Plugin com rota de API personalizada

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: {},
  });

Capacidades disponíveis

CapacidadeO que permite
read:contentLer conteúdo publicado
write:contentCriar e atualizar conteúdo
read:usersLer perfis de usuário
email:sendEnviar email através do provedor configurado
kv:readLer do namespace KV do plugin
kv:writeEscrever no namespace KV do plugin
http:fetchFazer requisições HTTP de saída
storage:readLer do armazenamento de mídia
storage:writeEscrever no armazenamento de mídia

Hooks disponíveis

// Ciclo de vida do conteúdo
"content:beforeSave"
"content:afterSave"
"content:beforeDelete"
"content:afterDelete"
"content:beforeRender"

// Ciclo de vida da autenticação
"auth:afterLogin"
"auth:afterLogout"
"auth:afterRegister"

// Ciclo de vida da mídia
"media:beforeUpload"
"media:afterUpload"
"media:beforeDelete"

// Ciclo de vida administrativo
"admin:init"

Autenticação

O EmDash usa autenticação baseada em passkeys (WebAuthn) com fallbacks OAuth e link mágico.

Configurar provedores 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,
          }),
        ],
      },
    }),
  ],
});

Configurar email de link mágico

auth: {
  providers: [
    magicLink({
      from: "[email protected]",
      // usa o adaptador de email configurado
    }),
  ],
}

Proteger uma página

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

const user = await requireAuth(Astro);
// Redireciona para /login se não autenticado
---

<p>Bem-vindo, {user.name}!</p>

Obter o usuário atual (opcional)

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

const user = await getUser(Astro); // null se não logado
---

{user ? <p>Olá {user.name}</p> : <a href="/login">Entrar</a>}

Papéis

import { requireRole } from "emdash/auth";

// Em uma rota de API ou página
const user = await requireRole(Astro, "editor");
// Papéis: "administrator" | "editor" | "author" | "contributor"

Migração do WordPress

Importar de um arquivo de exportação WXR

npx emdash import:wordpress ./export.xml

Importar da REST API do WordPress

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

Importar do WordPress.com

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

O importador migra posts, páginas, anexos de mídia, categorias, tags, autores e comentários. Os blocos Gutenberg são convertidos para Portable Text através do pacote gutenberg-to-portable-text.


Esquema de Conteúdo (UI Administrativa)

Os tipos de conteúdo são criados no painel administrativo em /_emdash/admin — não em código. Após criar ou modificar uma coleção, regenere os tipos TypeScript:

npx emdash types
# Escreve em src/emdash.d.ts

Tipos de campo disponíveis no construtor de esquema

  • text — string curta
  • richText — Portable Text (editor TipTap)
  • number — inteiro ou float
  • boolean — alternância verdadeiro/falso
  • date / datetime — seletores de data
  • select — menu suspenso de escolha única
  • multiSelect — escolha múltipla
  • image — seletor de biblioteca de mídia
  • file — anexo de arquivo
  • relation — link para outra entrada da coleção
  • slug — string segura para URL, gerada automaticamente a partir de um campo de origem
  • json — JSON bruto

Servidor MCP (Integração com Ferramentas de IA)

O EmDash inclui um servidor MCP integrado para se conectar a ferramentas de IA como Claude Code, Cursor e Codex. Assim que seu servidor de desenvolvimento Astro estiver rodando, você pode habilitar as ferramentas MCP do EmDash:

Usando o MCP Inspector

npx @modelcontextprotocol/inspector

Configurando com os agentes

Claude Code:

claude mcp add emdash -- npx @emdash/mcp-server

Cursor: Adicione o servidor nas configurações MCP.

Codex: Configure no arquivo .codex/settings.json.

Ferramentas MCP disponíveis

FerramentaDescrição
list_collectionsListar todas as coleções de conteúdo
get_collectionObter entradas de uma coleção com filtros
get_entryObter uma única entrada pelo slug
create_entryCriar uma nova entrada de conteúdo
update_entryAtualizar uma entrada existente
search_contentPesquisar em todas as coleções
run_migrationExecutar migrações de banco de dados
get_analyticsObter estatísticas básicas de conteúdo

Exemplo: consultar posts via MCP

claude mcp call emdash get_collection --params='{"name":"posts","filter":{"status":"published"},"limit":5}'

Isso retorna as últimas 5 entradas publicadas da coleção "posts" como JSON estruturado.


Licença

MIT — veja o arquivo LICENSE.


Precisa de ajuda? Junte-se ao servidor Discord do EmDash.

Habilidades Relacionadas