EmDash para desenvolvedores Astro

Nesta página

EmDash é um CMS construído especificamente para Astro. Ele estende seu site Astro com conteúdo armazenado em banco de dados, uma interface admin polida e recursos estilo WordPress (menus, widgets, taxonomias) enquanto preserva a experiência de desenvolvimento que você espera.

Tudo o que você sabe sobre Astro continua valendo. EmDash adiciona gerenciamento de conteúdo sobre seu fluxo de trabalho Astro existente.

O que o EmDash adiciona

EmDash fornece os recursos de gerenciamento de conteúdo que faltam em sites Astro baseados em arquivos:

RecursoDescrição
Interface adminInterface de edição WYSIWYG completa em /_emdash/admin
Armazenamento em BDConteúdo armazenado em SQLite, libSQL, Cloudflare D1 ou PostgreSQL
Biblioteca de mídiaUpload, organização e disponibilização de imagens e arquivos
Menus de navegaçãoGerenciamento de menus drag-and-drop com aninhamento
Áreas de widgetsBarras laterais dinâmicas e regiões de rodapé
Configurações do siteConfiguração global (título, logo, links sociais)
TaxonomiasCategorias, tags e taxonomias personalizadas
Sistema de previewURLs de preview assinadas para conteúdo em rascunho
RevisõesHistórico de versões do conteúdo

Astro Collections vs EmDash

As coleções astro:content do Astro são baseadas em arquivos e resolvidas em tempo de build. As coleções EmDash são armazenadas em banco de dados e resolvidas em tempo de execução.

Coleções AstroColeções EmDash
ArmazenamentoArquivos Markdown/MDX em src/content/Banco de dados SQL (SQLite, libSQL, D1 ou Postgres)
EdiçãoEditor de códigoInterface admin
Formato de conteúdoMarkdown com frontmatterPortable Text (JSON estruturado)
AtualizaçõesRequer rebuildInstantâneo (SSR)
SchemaZod em content.config.tsDefinido no admin, armazenado no BD
Ideal paraConteúdo gerenciado por desenvolvedoresConteúdo gerenciado por editores

Usar ambos juntos

As coleções Astro e EmDash podem coexistir. Use coleções Astro para conteúdo de desenvolvedores (docs, changelogs) e EmDash para conteúdo editorial (posts de blog, páginas):

---
import { getCollection } from "astro:content";
import { getEmDashCollection } from "emdash";

// Docs gerenciados por desenvolvedores a partir de arquivos
const docs = await getCollection("docs");

// Posts gerenciados por editores a partir do banco de dados
const { entries: posts } = await getEmDashCollection("posts", {
  status: "published",
  limit: 5,
});
---

Configuração

EmDash requer dois arquivos de configuração.

Integração Astro

A seguinte configuração registra EmDash como uma integração Astro em modo de saída server:

import { defineConfig } from "astro/config";
import react from "@astrojs/react";
import emdash, { local } from "emdash/astro";
import { sqlite } from "emdash/db";

export default defineConfig({
	output: "server", // Obrigatório para EmDash
	integrations: [
		react(), // Obrigatório — a interface admin é uma app React
		emdash({
			database: sqlite({ url: "file:./data.db" }),
			storage: local({
				directory: "./uploads",
				baseUrl: "/_emdash/api/media/file",
			}),
		}),
	],
});

Live Collections Loader

O seguinte arquivo registra EmDash como fonte de conteúdo ao vivo:

import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";

export const collections = {
	_emdash: defineLiveCollection({
		loader: emdashLoader(),
	}),
};

A coleção _emdash roteia internamente para seus tipos de conteúdo (posts, páginas, produtos).

Consultando conteúdo

EmDash fornece funções de consulta que seguem o padrão de Live Content Collections do Astro, retornando { entries, error } ou { entry, error }:

EmDash

import { getEmDashCollection, getEmDashEntry } from "emdash";

// Obter todos os posts publicados - retorna { entries, error }
const { entries: posts } = await getEmDashCollection("posts", {
status: "published",
});

// Obter um post por slug - retorna { entry, error, isPreview }
const { entry: post } = await getEmDashEntry("posts", "my-post");

Astro

import { getCollection, getEntry } from "astro:content";

// Obter todas as entradas do blog
const posts = await getCollection("blog");

// Obter uma entrada por slug
const post = await getEntry("blog", "my-post");

Opções de filtragem

getEmDashCollection suporta filtragem que getCollection do Astro não oferece:

const { entries: posts } = await getEmDashCollection("posts", {
	status: "published", // draft | published | archived
	limit: 10, // resultados máximos
	where: { category: "news" }, // filtro de taxonomia
});

Renderizando conteúdo

EmDash armazena texto rico como Portable Text, um formato JSON estruturado. Renderize-o com o componente PortableText:

EmDash

---
import { getEmDashEntry } from "emdash";
import { PortableText } from "emdash/ui";

const { slug } = Astro.params;
const { entry: post } = await getEmDashEntry("posts", slug);

if (!post) {
return Astro.redirect("/404");
}

---

<article>
  <h1>{post.data.title}</h1>
  <PortableText value={post.data.content} />
</article>

Astro

---
import { getEntry, render } from "astro:content";

const { slug } = Astro.params;
const post = await getEntry("blog", slug);
const { Content } = await render(post);

---

<article>
  <h1>{post.data.title}</h1>
  <Content />
</article>

Recursos dinâmicos

EmDash fornece APIs para recursos estilo WordPress que não existem na camada de conteúdo do Astro.

O seguinte layout busca um menu por localização e o renderiza com itens aninhados:

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

const primaryMenu = await getMenu("primary");
---

{primaryMenu && (
  <nav>
    <ul>
      {primaryMenu.items.map(item => (
        <li>
          <a href={item.url}>{item.label}</a>
          {item.children.length > 0 && (
            <ul>
              {item.children.map(child => (
                <li><a href={child.url}>{child.label}</a></li>
              ))}
            </ul>
          )}
        </li>
      ))}
    </ul>
  </nav>
)}

Áreas de widgets

O seguinte layout busca uma área de widgets e renderiza cada widget:

---
import { getWidgetArea } from "emdash";
import { PortableText } from "emdash/ui";

const sidebar = await getWidgetArea("sidebar");
---

{sidebar && sidebar.widgets.length > 0 && (
  <aside>
    {sidebar.widgets.map(widget => (
      <div class="widget">
        {widget.title && <h3>{widget.title}</h3>}
        {widget.type === "content" && widget.content && (
          <PortableText value={widget.content} />
        )}
      </div>
    ))}
  </aside>
)}

Configurações do site

O seguinte componente lê as configurações globais do site e renderiza um logo ou título:

---
import { getSiteSettings, getSiteSetting } from "emdash";

const settings = await getSiteSettings();
// Ou buscar valores individuais:
const title = await getSiteSetting("title");
---

<header>
  {settings.logo ? (
    <img src={settings.logo.url} alt={settings.title} />
  ) : (
    <span>{settings.title}</span>
  )}
  {settings.tagline && <p>{settings.tagline}</p>}
</header>

Plugins

Estenda EmDash com plugins que adicionam hooks, armazenamento, configurações e interface admin:

import react from "@astrojs/react";
import emdash from "emdash/astro";
import seoPlugin from "@emdash-cms/plugin-seo";

export default defineConfig({
	integrations: [
		react(),
		emdash({
			// ...
			plugins: [seoPlugin({ generateSitemap: true })],
		}),
	],
});

Crie plugins personalizados com definePlugin:

import { definePlugin } from "emdash";

export default definePlugin({
	id: "analytics",
	version: "1.0.0",
	capabilities: ["content:read"],

	hooks: {
		"content:afterSave": async (event, ctx) => {
			ctx.log.info("Content saved", { id: event.content.id });
		},
	},

	admin: {
		settingsSchema: {
			trackingId: { type: "string", label: "ID de rastreamento" },
		},
	},
});

Renderização no servidor

Sites EmDash rodam em modo SSR, então o conteúdo é servido em tempo de execução e as alterações aparecem imediatamente.

Para páginas estáticas com getStaticPaths, o conteúdo é buscado em tempo de build:

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

export async function getStaticPaths() {
  const { entries: posts } = await getEmDashCollection("posts", {
    status: "published",
  });

  return posts.map((post) => ({
    params: { slug: post.data.slug },
  }));
}

const { slug } = Astro.params;
const { entry: post } = await getEmDashEntry("posts", slug);
---

Para páginas dinâmicas, defina prerender = false para buscar conteúdo a cada requisição:

---
export const prerender = false;

import { getEmDashEntry } from "emdash";

const { slug } = Astro.params;
const { entry: post, error } = await getEmDashEntry("posts", slug);

if (error) {
  return new Response("Server error", { status: 500 });
}

if (!post) {
  return new Response(null, { status: 404 });
}
---

Próximos passos