Block Kit

Nesta página

O Block Kit do EmDash permite que plugins sandboxed descrevam sua UI de administração como JSON. O host renderiza os blocos — nenhum JavaScript fornecido pelo plugin é executado no navegador.

Como funciona

  1. O usuário navega até a página de administração de um plugin.
  2. A administração envia uma interação page_load para a rota de administração do plugin.
  3. O plugin retorna um BlockResponse contendo um array de blocos.
  4. A administração renderiza os blocos com o componente BlockRenderer.
  5. Quando o usuário interage (clica em um botão, envia um formulário), a administração envia a interação de volta ao plugin.
  6. O plugin retorna novos blocos e o ciclo se repete.

Adicione @emdash-cms/blocks e zod ao plugin quando ele definir uma página Block Kit:

pnpm add @emdash-cms/blocks zod

Declare a página no manifesto do plugin para que a administração tenha uma entrada de navegação para carregar:

"admin": {
	"pages": [{ "path": "/settings", "label": "Settings", "icon": "settings" }],
}

A seguinte rota admin valida a interação, renderiza um formulário no carregamento da página e armazena seus valores no envio:

import type { SandboxedPlugin } from "emdash/plugin";
import type { BlockResponse } from "@emdash-cms/blocks";
import { z } from "zod";

const interactionSchema = z.discriminatedUnion("type", [
	z.object({ type: z.literal("page_load"), page: z.string() }),
	z.object({
		type: z.literal("block_action"),
		action_id: z.string(),
		block_id: z.string().optional(),
		value: z.unknown().optional(),
	}),
	z.object({
		type: z.literal("form_submit"),
		action_id: z.string(),
		block_id: z.string().optional(),
		values: z.object({ api_url: z.url(), enabled: z.boolean() }),
	}),
]);

function renderSettings(): BlockResponse {
	return {
		blocks: [
			{ type: "header", text: "Save Log settings" },
			{
				type: "form",
				block_id: "settings",
				fields: [
					{ type: "text_input", action_id: "api_url", label: "API URL" },
					{ type: "toggle", action_id: "enabled", label: "Enabled", initial_value: true },
				],
				submit: { label: "Save", action_id: "save" },
			},
		],
	};
}

const plugin: SandboxedPlugin = {
	routes: {
		admin: {
			handler: async (routeCtx, ctx) => {
				const parsed = interactionSchema.safeParse(routeCtx.input);
				if (!parsed.success) return { blocks: [] };
				const interaction = parsed.data;

				if (interaction.type === "page_load") {
					return renderSettings();
				}

				if (interaction.type === "form_submit" && interaction.action_id === "save") {
					await ctx.settings.set("apiUrl", interaction.values.api_url);
					await ctx.settings.set("enabled", interaction.values.enabled);
					return {
						...renderSettings(),
						toast: { message: "Settings saved", type: "success" },
					};
				}

				return { blocks: [] };
			},
		},
	},
};

export default plugin;

A rota admin é privada por padrão. O EmDash envia o cabeçalho CSRF correto quando a administração a chama. O manipulador ainda valida routeCtx.input porque seu tipo TypeScript é unknown e um chamador pode invocar uma rota privada do plugin fora da página Block Kit.

O EmDash valida cada resposta de página e widget antes de a administração renderizá-la. Um bloco inválido, URL insegura, link para uma página de plugin não declarada ou resposta acima dos limites do Block Kit falha a solicitação em vez de chegar ao navegador. Uma resposta pode conter até 256 KiB, 20 níveis aninhados, 2.000 nós, 1.000 itens por array e 64 KiB por string.

Locale e direção da UI

Leia routeCtx.ui quando uma página ou widget precisar retornar texto para o locale ativo do administrador. O host deriva esse valor do cookie de locale da administração ou do idioma da solicitação e verifica a página ou widget solicitado em relação ao manifesto do plugin.

import type { SandboxedPlugin } from "emdash/plugin";

const plugin: SandboxedPlugin = {
	routes: {
		admin: {
			handler: async (routeCtx) => {
				if (!routeCtx.ui) return { blocks: [] };

				const heading = routeCtx.ui.locale === "ar" ? "حالة المحتوى" : "Content status";
				return {
					blocks: [{ type: "header", text: heading }],
				};
			},
		},
	},
};

export default plugin;

routeCtx.ui contém a superfície, o locale e a direção do texto. O locale da administração é separado de ctx.site.locale, que descreve o locale de conteúdo padrão do site. Os rótulos do manifesto permanecem strings estáticas.

Use um elemento link para navegar sem despachar uma ação Block Kit. O EmDash constrói URLs internas a partir de alvos estruturados, de modo que os plugins não precisam conhecer os caminhos das rotas de administração.

return {
	blocks: [
		{
			type: "actions",
			elements: [
				{
					type: "link",
					label: "Edit article",
					target: { kind: "content", collection: "posts", id: "01K5POSTEXAMPLE", locale: "en" },
					appearance: "primary",
				},
				{
					type: "link",
					label: "Plugin settings",
					target: { kind: "plugin-settings" },
				},
			],
		},
	],
};

Os alvos disponíveis são:

  • content, com uma coleção, ID de entrada salva e locale de conteúdo opcional;
  • plugin-page, com um caminho declarado pelo mesmo plugin;
  • plugin-settings; e
  • external, com uma URL absoluta HTTP, HTTPS ou mailto:.

Links externos abrem em uma nova aba com noopener noreferrer. Elementos link não aceitam action_id e não podem aparecer como campos de formulário. Use um botão quando a interação precisar chamar a rota do plugin.

Imagens de bloco usam a mesma política de recursos do navegador. URLs de imagem relativas à raiz são permitidas. Uma imagem externa deve usar HTTPS e seu hostname deve aparecer em allowedHosts do plugin. Um plugin com network:request:unrestricted pode carregar uma imagem HTTPS de qualquer hostname. Outras imagens externas fazem a resposta completa do Block Kit ser rejeitada.

Ações de linha em tabelas

Defina o format de uma coluna da tabela como element para colocar um botão, link ou menu em cada linha. Cada linha armazena o elemento sob a chave da coluna; uma linha sem valor deixa a célula vazia. Use um elemento menu quando uma linha oferecer várias escolhas atrás de um botão:

return {
	blocks: [
		{
			type: "table",
			page_action_id: "missing_page",
			columns: [
				{ key: "title", label: "Entry" },
				{ key: "languages", label: "Missing" },
				{ key: "action", label: "Actions", format: "element" },
			],
			rows: [
				{
					title: "Hello world",
					languages: "French, Italian",
					action: {
						type: "menu",
						action_id: "translate",
						label: "Translate",
						items: [
							{ label: "French", value: "fr:01K5POSTEXAMPLE" },
							{ label: "Italian", value: "it:01K5POSTEXAMPLE" },
						],
					},
				},
			],
		},
	],
};

Escolher um item do menu envia um block_action com o action_id do menu e o value do item. Os valores dos itens devem ser únicos dentro de um menu. Células de elemento aceitam apenas elementos button, link e menu. Um menu também pode aparecer em um bloco actions, como acessório de seção ou em ações de estado vazio, mas não como campo de formulário. O builder elements.menu(actionId, label, items, { style }) retorna a mesma forma.

Painéis e ações de entradas salvas

Declare um painel do editor quando um plugin precisar mostrar informações ao lado de uma entrada salva. Os painéis começam recolhidos e chamam sua rota privada apenas quando um editor os abre.

O manifesto a seguir adiciona um painel para publicações e uma ação de reparo confirmada:

"admin": {
	"editorPanels": [
		{
			"id": "content-health",
			"title": "Content health",
			"route": "editor/content-health",
			"collections": ["posts"],
			"draft": {
				"read": { "translatable": true },
				"patch": { "fields": ["title", "excerpt", "body"] },
			},
		},
	],
	"editorActions": [
		{
			"id": "repair-metadata",
			"label": "Repair metadata",
			"route": "editor/repair-metadata",
			"placement": "overflow",
			"style": "danger",
			"confirm": {
				"title": "Repair metadata?",
				"text": "This changes the saved entry.",
				"confirm": "Repair",
				"deny": "Cancel",
			},
		},
	],
}

Cada rota referenciada deve ser privada. Sua permission controla quais editores podem invocar a extensão. O host também recarrega a entrada salva e verifica seu proprietário antes de chamar o plugin.

Rotas de extensão do editor recebem um valor routeCtx.ui atestado. Para as superfícies content-editor-panel e content-editor-action, routeCtx.ui.entry contém a coleção, o ID da entrada salva, o locale de conteúdo e a versão. routeCtx.ui.extensionId identifica a declaração selecionada. Use ctx.content com a capacidade content:read quando o plugin precisar de conteúdo salvo.

Um painel recebe { type: "panel_load" } quando abre. O carregamento do painel nunca inclui dados de rascunho. Suas interações posteriores de botão e formulário usam as formas habituais block_action e form_submit. Quando o plugin declara admin.editor-draft:read e a extensão restringe draft.read, uma interação explícita também recebe routeCtx.input.draft. O snapshot contém apenas valores atuais selecionados, definições de campo sanitizadas, identidade salva e a revisão base persistida. Use fields para slugs explícitos, translatable: true para os campos traduzíveis da coleção, ou ambos. O acesso ao rascunho exige uma lista collections explícita.

admin.editor-draft:patch é independente do acesso de leitura. Permite que uma rota retorne um patch de campo completo após uma interação explícita:

const draft = routeCtx.input.draft;

return {
	blocks: [],
	patch: {
		type: "editor-draft-patch",
		operations: [
			{ op: "set", field: "title", value: translate(draft.fields.title) },
			{ op: "clear", field: "excerpt" },
		],
	},
};

O EmDash valida cada operação em conjunto contra o esquema atual do servidor, capacidade, coleção, seletor de campo, locale, revisão base, propriedade, limites de contagem e limites de bytes. O navegador repete verificações de identidade, geração e campo antes de mostrar uma prévia renderizada pelo host. Aplicar a prévia marca o formulário como modificado e não salva, não cria uma revisão e não executa hooks. Qualquer edição feita enquanto o plugin trabalha rejeita o resultado completo.

Ações do editor apenas para salvos permanecem desabilitadas enquanto o formulário tem alterações não salvas. Ações cientes de rascunho podem ser executadas contra o formulário não salvo. Uma ação recebe { type: "editor_action" } e, quando declarada, o mesmo snapshot de rascunho delimitado. Retorne um objeto contendo um toast opcional e no máximo um efeito terminal:

return {
	toast: { type: "success", message: "Metadata repaired" },
	refresh: true,
};

Use refresh: true para recarregar a entrada, navigate com um alvo de link estruturado ou patch para propor alterações de campo não salvas. Uma resposta não pode combinar efeitos terminais. O EmDash rejeita comandos desconhecidos, navegação insegura, patches inválidos ou obsoletos e respostas acima dos limites do Block Kit antes de aplicar um efeito.

Tipos de bloco

TypeDescription
headerCabeçalho grande em negrito
sectionTexto com elemento acessório opcional
dividerLinha horizontal
fieldsGrade rótulo/valor de duas colunas
tableTabela de dados com formatação, ordenação, paginação
actionsLinha horizontal de botões e controles
statsCartões de métricas do painel com indicadores de tendência
formCampos de entrada com visibilidade condicional e envio
imageImagem em nível de bloco com texto alternativo e título opcional
contextTexto de ajuda pequeno e atenuado
columnsLayout de 2–3 colunas com blocos aninhados
emptyTítulo de estado vazio com descrição, comando e botões de ação opcionais
accordionSeção recolhível envolvendo blocos aninhados
chartSérie temporal de linhas ou barras, ou gráfico com opções personalizadas
bannerMensagem de status ou alerta com título ou descrição
meterValor numérico exibido em relação a um mínimo e um máximo
codeCódigo TypeScript, TSX, JSONC, Bash ou CSS somente leitura
tabPainéis rotulados contendo blocos aninhados

Tipos de elemento

TypeDescription
buttonBotão de ação com diálogo de confirmação opcional
linkNavegação interna ou externa resolvida pelo host
menuBotão que abre uma lista de opções; cada opção dispara uma ação
text_inputEntrada de texto de uma ou várias linhas
number_inputEntrada numérica com min/max
selectSeleção suspensa
toggleInterruptor liga/desliga
secret_inputEntrada mascarada para chaves de API e tokens
checkboxSelecionar vários valores de uma lista fixa
comboboxSeleção de valor único pesquisável
date_inputValor de data
radioEscolha única de uma lista de opções visível

O editor de campos Portable Text também oferece repeater e media_picker. Eles não são campos de formulário para uma página de administração de plugin sandboxed.

Auxiliares de builder

O pacote @emdash-cms/blocks exporta as mesmas formas pelos objetos builder blocks e elements. Builders reduzem erros de nomes de propriedades e retornam objetos ordinários compatíveis com JSON:

import { blocks, elements } from "@emdash-cms/blocks";

const { header, form } = blocks;
const { textInput, toggle, select, link } = elements;

return {
	blocks: [
		header("SEO Settings"),
		form({
			blockId: "settings",
			fields: [
				textInput("site_title", "Site Title", { initialValue: "My Site" }),
				toggle("generate_sitemap", "Generate Sitemap", { initialValue: true }),
				select("robots", "Default Robots", [
					{ label: "Index, Follow", value: "index,follow" },
					{ label: "No Index", value: "noindex,follow" },
				]),
			],
			submit: { label: "Save", actionId: "save" },
		}),
		blocks.actions([link("Open settings", { kind: "plugin-page", path: "/settings" })]),
	],
};

Campos condicionais

Campos de formulário podem ser mostrados condicionalmente com base em outros valores de campo:

{
	"type": "toggle",
	"action_id": "auth_enabled",
	"label": "Enable Authentication"
}
{
	"type": "secret_input",
	"action_id": "api_key",
	"label": "API Key",
	"condition": { "field": "auth_enabled", "eq": true }
}

O campo api_key só aparece quando auth_enabled está ativado. As condições são avaliadas no cliente sem ida e volta.

secret_input usa has_value: true para mostrar que um valor já existe; não aceita nem retorna o valor armazenado no carregamento da página. O campo mascara a digitação no navegador. Declare a chave correspondente como type: "secret" em admin.settingsSchema e salve-a por ctx.settings para que o EmDash a criptografe. Siga Secret settings antes de armazenar credenciais.

Experimente

Use o Block Playground para construir e testar layouts de blocos de forma interativa.