Esta página é para pessoas trabalhando no EmDash, não construindo um site com ele. Ela explica o layout do banco de dados, a integração Astro, os caminhos de requisição, a aplicação de administração, o fluxo de mídia e o sistema de importação. Se você está construindo um site, leia Arquitetura e o Modelo de conteúdo em vez disso.
A integração Astro
O EmDash roda como uma integração Astro do pacote emdash. No tempo de build:
-
Injeta a aplicação de administração e as rotas da API REST com a API
injectRoutedo Astro. Nada é copiado para o projeto do usuário. As principais famílias de rotas são:Padrão de caminho Propósito /_emdash/admin/[...path]SPA do painel de administração /_emdash/api/manifestManifesto do admin (coleções, plugins) /_emdash/api/content/[collection]/...Operações de entradas de conteúdo /_emdash/api/media/...Operações da biblioteca de mídia /_emdash/api/schema/...Gerenciamento de esquema /_emdash/api/settings/...Configurações do site /_emdash/api/menus/...Menus de navegação /_emdash/api/taxonomies/...Categorias, tags, taxonomias customizadas /_emdash/api/plugins/[pluginId]/[...path]Rotas de API definidas por plugins O injetor de rotas é o inventário completo, incluindo autenticação, comentários, busca, importações, widgets e outras famílias de rotas.
-
Gera módulos virtuais para que o bundler possa resolver código de configuração e extensão:
Módulo Propósito virtual:emdash/configConfiguração de banco de dados, armazenamento e site virtual:emdash/dialectFactory do dialeto de banco de dados virtual:emdash/admin-registryImportações estáticas para interfaces admin de plugins virtual:emdash/pluginsImplementações de plugins configurados virtual:emdash/media-providersProvedores de mídia externos configurados virtual-modules.tsdefine os helpers de runtime restantes e os conteúdos de módulos gerados. -
Fornece o loader de Live Content Collections e registra o middleware de runtime. No tempo de requisição, o middleware abre as conexões de banco de dados e armazenamento configuradas e aplica quaisquer migrações pendentes antes que as rotas as usem.
Esquema database-first
As definições de esquema residem no banco de dados, não em um arquivo de configuração estático. _emdash_collections armazena uma linha por coleção. Suas colunas principais descrevem a coleção e os recursos que o runtime e o admin expõem:
| Colunas | Propósito |
|---|---|
id, slug | Identidade estável da coleção |
label, label_singular, description, icon | Nomes e orientações mostrados aos editores |
supports, has_seo, comments_enabled, edit_locking | Capacidades opcionais da coleção |
title_field, date_field, admin_config, hidden, sort_order | Comportamento da lista admin e navegação |
url_pattern, routable | URL pública e comportamento do slug |
source | Como a coleção foi criada |
O valor source registra a proveniência como manual, seed, template:<name>, import:<name> ou discovered. Configurações adicionais vêm de migrações registradas, então database/types.ts e as migrações são o inventário atual de colunas.
_emdash_fields armazena os campos vinculados a cada coleção:
| Colunas | Propósito |
|---|---|
id, collection_id, slug | Identidade do campo e coleção proprietária |
label, type, column_type | Rótulo do editor, tipo de campo EmDash e tipo de armazenamento SQL |
required, unique, default_value, validation | Restrições de conteúdo e padrões |
widget, options, sort_order | Controle do editor e ordem de exibição |
searchable, indexed, translatable | Comportamento de busca, consulta e localização |
collection_id referencia _emdash_collections.id, e cada slug de campo é único dentro de sua coleção.
Tabelas de conteúdo por coleção
Cada coleção recebe sua própria tabela, com prefixo ec_. Uma coleção products com campos title e price produz uma tabela com esta forma:
CREATE TABLE ec_products (
-- Colunas do sistema, presentes em toda tabela de conteúdo
id TEXT PRIMARY KEY,
slug TEXT,
status TEXT DEFAULT 'draft',
author_id TEXT,
primary_byline_id TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
published_at TEXT,
scheduled_at TEXT,
deleted_at TEXT,
version INTEGER DEFAULT 1,
live_revision_id TEXT,
draft_revision_id TEXT,
locale TEXT NOT NULL DEFAULT 'en',
translation_group TEXT,
-- Colunas de conteúdo, criadas a partir de definições de campos
title TEXT NOT NULL,
price REAL,
UNIQUE (slug, locale)
);
Colunas reais dão a cada campo um tipo de banco de dados, permitem índices e chaves estrangeiras, e deixam ferramentas de banco de dados inspecionar o esquema sem decodificar um blob JSON de conteúdo. A restrição unique permite que traduções compartilhem um slug enquanto mantêm cada slug único dentro de uma localidade. Todas as variantes de localidade da mesma entrada compartilham um valor translation_group, que permite ao EmDash encontrar as linhas que são traduções umas das outras.
As principais preocupações de dados permanecem separadas:
| Preocupação | Localização | Tabelas |
|---|---|---|
| Esquema | Tabelas do sistema | _emdash_collections, _emdash_fields |
| Conteúdo | Tabelas por coleção | ec_posts, ec_products, … |
| Mídia | Tabela separada + armazenamento | Tabela media + armazenamento configurado |
| Configurações | Tabela de opções | options com prefixo site: |
Mudanças de esquema em runtime
Adicionar um campo pela UI de administração executa estes passos:
- Inserir a definição do campo em
_emdash_fields. - Adicionar a coluna correspondente à tabela
ec_*da coleção e criar um índice quando o campo é configurado como indexado. - Atualizar os tipos de desenvolvimento gerados para que o novo campo apareça nas ferramentas do editor.
A validação de conteúdo lê as definições de campos atuais e constrói um esquema Zod quando conteúdo é criado ou atualizado. Alterar o tipo SQL subjacente, a restrição required ou unique, ou o comportamento de localização de um campo pode requerer uma migração de conteúdo manual; SchemaRegistry rejeita alterações in loco não suportadas em vez de reconstruir a tabela implicitamente.
Validação em runtime
O EmDash deriva um esquema Zod dos campos atuais da coleção. O gerador delega os detalhes de tipo e restrição para generateFieldSchema():
export function generateZodSchema(
collection: CollectionWithFields,
): z.ZodObject<Record<string, ZodType>> {
const shape: Record<string, ZodType> = {};
for (const field of collection.fields) {
shape[field.slug] = generateFieldSchema(field);
}
return z.object(shape);
}
O handler de conteúdo também rejeita campos desconhecidos, verifica valores de string obrigatórios e verifica referências a outras coleções.
Camada de dados
O EmDash usa Kysely para SQL tipado através de SQLite, libSQL, Cloudflare D1 e PostgreSQL. A configuração do site seleciona o adaptador de banco de dados; a integração expõe sua factory de dialeto através de virtual:emdash/dialect.
Loader de Live Content Collections
O conteúdo é servido em runtime através das Live Content Collections do Astro. emdashLoader() implementa a interface LiveLoader do Astro e é registrado como uma única coleção _emdash:
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({ loader: emdashLoader() }),
};
A coleção única _emdash envolve cada coleção do EmDash. getEmDashCollection("posts") fornece o filtro de tipo posts, e o loader o mapeia para a tabela ec_posts.
Caminhos de requisição
Uma requisição de conteúdo de uma página Astro segue este caminho:
- A página chama
getEmDashCollection()ougetEmDashEntry(). - O wrapper de consulta chama
getLiveCollection()ougetLiveEntry()do Astro com a coleção interna_emdashe o tipo de coleção EmDash solicitado. emdashLoader()consulta a tabelaec_*relevante através do Kysely, aplicando regras de publicação, localidade, filtro, ordenação e paginação.- O wrapper de consulta mapeia as linhas para entradas Astro e carrega seus bylines e termos de taxonomia.
- O componente Astro renderiza as entradas retornadas.
O estado de pré-visualização e modo de edição viaja através do contexto de requisição, então as mesmas funções de consulta podem retornar conteúdo de rascunho depois que o middleware verifica a requisição.
Uma requisição da API de administração segue um caminho separado:
- O middleware autentica a requisição e armazena o usuário resolvido em
Astro.locals. - A rota da API analisa a requisição e verifica a permissão necessária para essa operação.
- A rota delega a lógica de negócio a um handler ou repositório.
- O handler executa hooks de ciclo de vida de plugins em torno da operação de banco de dados quando essa operação expõe hooks.
- A rota retorna uma resposta JSON padrão de sucesso ou erro para a aplicação de administração.
Internos do painel de administração
O admin é uma aplicação React de página única. O Astro serve seu shell e o middleware de autenticação protege as rotas de administração. Dentro da aplicação, TanStack Router gerencia a navegação, TanStack Query carrega o estado do servidor, TanStack Table renderiza grids de dados, React Hook Form e Zod gerenciam formulários, TipTap edita Portable Text e Kumo fornece o sistema de design.
Para autenticação de sessão, o middleware redireciona uma requisição de navegador não autenticada para a página de login e retorna um erro JSON para uma requisição de API não autenticada. Depois de carregar um usuário ativo, ele o coloca em Astro.locals para a rota:
const sessionUser = await resolveSessionUser(session);
if (!sessionUser?.id) {
if (isApiRoute) {
return apiError("NOT_AUTHENTICATED", "Not authenticated", 401);
}
const loginUrl = new URL("/_emdash/admin/login", getPublicOrigin(url, emdash?.config));
loginUrl.searchParams.set("redirect", url.pathname);
return context.redirect(loginUrl.toString());
}
Após este branch, o middleware carrega o usuário, rejeita contas ausentes ou desabilitadas, coloca o usuário ativo em Astro.locals e continua para a rota.
UI orientada por manifesto
O admin não codifica esquemas de coleção ou contribuições de plugins. Ele busca GET /_emdash/api/manifest, que descreve as coleções atuais, campos, plugins, taxonomias, modo de autenticação e outras capacidades configuradas. Um manifesto abreviado parece assim:
{
"collections": {
"posts": {
"label": "Blog Posts",
"labelSingular": "Post",
"supports": ["drafts", "revisions", "preview"],
"fields": {
"title": { "kind": "string", "label": "Title", "required": true }
}
}
},
"plugins": {
"audit-log": { "version": "0.2.1", "enabled": true }
},
"taxonomies": [
{ "name": "category", "label": "Categories", "hierarchical": true }
],
"version": "0.37.0"
}
O admin usa o manifesto para construir a navegação de coleções e os editores de campos. Como o endpoint lê o esquema ao vivo, as mudanças de coleções e campos aparecem sem reconstruir a aplicação de administração.
UIs admin de plugins
Os pontos de entrada admin de plugins configurados são coletados em virtual:emdash/admin-registry. O módulo gerado usa importações estáticas para que o bundler possa incluir os componentes React:
import * as pluginAdmin0 from "@emdash-cms/plugin-seo/admin";
export const pluginAdmins = { seo: pluginAdmin0 };
Conversão de texto rico
Campos de Portable Text usam TipTap, que é baseado em ProseMirror. O EmDash converte Portable Text para ProseMirror quando o editor carrega e converte de volta para Portable Text quando a entrada é salva. Blocos desconhecidos de plugins ou importações são preservados como placeholders somente leitura em vez de serem descartados.
Uploads assinados
Uploads de mídia usam URLs assinadas diretas para o armazenamento quando o adaptador de armazenamento as suporta e um endpoint de streaming same-origin caso contrário:
- O cliente solicita um alvo de upload de
POST /_emdash/api/media/upload-url. O EmDash cria um item de mídia pendente. - O cliente faz upload para o alvo retornado. Adaptadores compatíveis com S3 podem retornar uma URL assinada que contorna os limites de tamanho do corpo da aplicação; bindings nativos R2 e armazenamento local retornam um endpoint de streaming EmDash.
- O cliente confirma o upload com
POST /_emdash/api/media/:id/confirm. - O EmDash valida o arquivo armazenado e marca o item de mídia como pronto.
Extensão do importador de conteúdo
O importador do WordPress usa uma interface ImportSource plugável. Uma fonte pode sondar uma URL, analisar o conteúdo disponível contra o esquema atual e transmitir itens de conteúdo normalizados:
interface ImportSource {
id: string;
name: string;
description: string;
icon: "upload" | "globe" | "wordpress" | "plug";
requiresFile?: boolean;
canProbe?: boolean;
probe?(url: string): Promise<SourceProbeResult | null>;
analyze(input: SourceInput, context: ImportContext): Promise<ImportAnalysis>;
fetchContent(input: SourceInput, options: FetchOptions): AsyncGenerator<NormalizedItem>;
fetchMedia?(url: string, input: SourceInput): Promise<Blob>;
}
A fonte WXR importa arquivos de exportação do WordPress. A fonte de conector importa diretamente de sites com o plugin EmDash para WordPress. Uma fonte REST separada detecta sites WordPress públicos, mas direciona o usuário para uma exportação WXR porque a importação REST direta não está implementada. Registre outra fonte quando um importador puder produzir as mesmas formas de análise normalizada e itens de conteúdo.