Block Kit

In questa pagina

Il Block Kit di EmDash consente ai plugin sandbox di descrivere la propria UI di amministrazione come JSON. L’host renderizza i blocchi: nessun JavaScript fornito dal plugin viene mai eseguito nel browser.

Come funziona

  1. L’utente naviga alla pagina di amministrazione di un plugin.
  2. L’admin invia un’interazione page_load alla route di amministrazione del plugin.
  3. Il plugin restituisce un BlockResponse contenente un array di blocchi.
  4. L’admin renderizza i blocchi con il componente BlockRenderer.
  5. Quando l’utente interagisce (fa clic su un pulsante, invia un modulo), l’admin rinvia l’interazione al plugin.
  6. Il plugin restituisce nuovi blocchi e il ciclo si ripete.

Aggiungi @emdash-cms/blocks e zod al plugin quando definisce una pagina Block Kit:

pnpm add @emdash-cms/blocks zod

Dichiara la pagina nel manifesto del plugin affinché l’admin abbia una voce di navigazione da caricare:

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

La seguente route admin valida l’interazione, renderizza un modulo al caricamento della pagina e memorizza i suoi valori all’invio:

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;

La route admin è privata per impostazione predefinita. EmDash invia l’header CSRF corretto quando l’admin la chiama. Il gestore valida comunque routeCtx.input perché il suo tipo TypeScript è unknown e un chiamante può invocare una route privata del plugin fuori dalla pagina Block Kit.

EmDash valida ogni risposta di pagina e widget prima che l’admin la renderizzi. Un blocco non valido, un URL non sicuro, un collegamento a una pagina del plugin non dichiarata o una risposta oltre i limiti Block Kit fa fallire la richiesta invece di raggiungere il browser. Una risposta può contenere fino a 256 KiB, 20 livelli annidati, 2.000 nodi, 1.000 elementi per array e 64 KiB per stringa.

Locale e direzione dell’UI

Leggi routeCtx.ui quando una pagina o un widget deve restituire testo per il locale attivo dell’amministratore. L’host deriva questo valore dal cookie del locale di amministrazione o dalla lingua della richiesta e verifica la pagina o il widget richiesto rispetto al manifesto del 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 contiene la superficie, il locale e la direzione del testo. Il locale di amministrazione è distinto da ctx.site.locale, che descrive il locale di contenuto predefinito del sito. Le etichette del manifesto restano stringhe statiche.

Collegamenti di navigazione

Usa un elemento link per navigare senza inviare un’azione Block Kit. EmDash costruisce URL interni da destinazioni strutturate, così i plugin non devono conoscere i percorsi delle route di amministrazione.

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

Le destinazioni disponibili sono:

  • content, con una collection, ID voce salvata e locale di contenuto opzionale;
  • plugin-page, con un percorso dichiarato dallo stesso plugin;
  • plugin-settings; e
  • external, con un URL assoluto HTTP, HTTPS o mailto:.

I collegamenti esterni si aprono in una nuova scheda con noopener noreferrer. Gli elementi link non accettano action_id e non possono comparire come campi di modulo. Usa un pulsante quando l’interazione deve chiamare la route del plugin.

Le immagini dei blocchi usano la stessa politica delle risorse del browser. Sono consentiti URL di immagine relativi alla radice. Un’immagine esterna deve usare HTTPS e il suo hostname deve comparire in allowedHosts del plugin. Un plugin con network:request:unrestricted può caricare un’immagine HTTPS da qualsiasi hostname. Altre immagini esterne fanno rifiutare l’intera risposta Block Kit.

Azioni di riga nelle tabelle

Impostate il format di una colonna della tabella su element per collocare un pulsante, un link o un menu in ogni riga. Ogni riga memorizza l’elemento sotto la chiave della colonna; una riga senza valore lascia la cella vuota. Usate un elemento menu quando una riga offre più scelte dietro un pulsante:

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

Scegliere una voce del menu invia un block_action con l’action_id del menu e il value della voce. I valori delle voci devono essere univoci all’interno di un menu. Le celle elemento accettano solo elementi button, link e menu. Un menu può comparire anche in un blocco actions, come accessorio di sezione o nelle azioni di stato vuoto, ma non come campo del form. Il builder elements.menu(actionId, label, items, { style }) restituisce la stessa forma.

Pannelli e azioni delle voci salvate

Dichiara un pannello dell’editor quando un plugin deve mostrare informazioni accanto a una voce salvata. I pannelli partono compressi e chiamano la loro route privata solo quando un editor li apre.

Il seguente manifesto aggiunge un pannello per i post e un’azione di riparazione confermata:

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

Ogni route referenziata deve essere privata. La sua permission controlla quali editor possono invocare l’estensione. L’host ricarica anche la voce salvata e ne verifica il proprietario prima di chiamare il plugin.

Le route di estensione dell’editor ricevono un valore routeCtx.ui attestato. Per le superfici content-editor-panel e content-editor-action, routeCtx.ui.entry contiene la collection, l’ID voce salvata, il locale di contenuto e la versione. routeCtx.ui.extensionId identifica la dichiarazione selezionata. Usa ctx.content con la capability content:read quando il plugin necessita del contenuto salvato.

Un pannello riceve { type: "panel_load" } quando si apre. Il caricamento del pannello non include mai dati di bozza. Le successive interazioni di pulsante e modulo usano le forme abituali block_action e form_submit. Quando il plugin dichiara admin.editor-draft:read e l’estensione restringe draft.read, un’interazione esplicita riceve anche routeCtx.input.draft. Lo snapshot contiene solo i valori correnti selezionati, definizioni di campo sanificate, identità salvata e la revisione di base persistita. Usa fields per slug espliciti, translatable: true per i campi traducibili della collection, o entrambi. L’accesso alla bozza richiede un elenco collections esplicito.

admin.editor-draft:patch è indipendente dall’accesso in lettura. Consente a una route di restituire una patch a livello di campo dopo un’interazione esplicita:

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

EmDash valida ogni operazione insieme rispetto allo schema server corrente, capability, collection, selettore di campo, locale, revisione di base, proprietà, limiti di conteggio e limiti di byte. Il browser ripete i controlli di identità, generazione e campo prima di mostrare un’anteprima renderizzata dall’host. Applicare l’anteprima contrassegna il modulo come modificato e non salva, non crea una revisione e non esegue hook. Qualsiasi modifica fatta mentre il plugin lavora rifiuta il risultato completo.

Le azioni dell’editor solo per il salvato restano disabilitate finché il modulo ha modifiche non salvate. Le azioni consapevoli della bozza possono eseguirsi contro il modulo non salvato. Un’azione riceve { type: "editor_action" } e, quando dichiarata, lo stesso snapshot di bozza delimitato. Restituisci un oggetto contenente un toast opzionale e al massimo un effetto terminale:

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

Usa refresh: true per ricaricare la voce, navigate con una destinazione di collegamento strutturata o patch per proporre modifiche di campo non salvate. Una risposta non può combinare effetti terminali. EmDash rifiuta comandi sconosciuti, navigazione non sicura, patch non valide o obsolete e risposte oltre i limiti Block Kit prima di applicare un effetto.

Tipi di blocco

TypeDescription
headerIntestazione grande in grassetto
sectionTesto con elemento accessorio opzionale
dividerRiga orizzontale
fieldsGriglia etichetta/valore a due colonne
tableTabella dati con formattazione, ordinamento, paginazione
actionsRiga orizzontale di pulsanti e controlli
statsSchede metriche della dashboard con indicatori di tendenza
formCampi di input con visibilità condizionale e invio
imageImmagine a livello di blocco con testo alternativo e titolo opzionale
contextPiccolo testo di aiuto attenuato
columnsLayout a 2–3 colonne con blocchi annidati
emptyTitolo di stato vuoto con descrizione, comando e pulsanti di azione opzionali
accordionSezione comprimibile che avvolge blocchi annidati
chartSerie temporale a linee o barre, o grafico con opzioni personalizzate
bannerMessaggio di stato o avviso con titolo o descrizione
meterValore numerico mostrato rispetto a un minimo e un massimo
codeCodice TypeScript, TSX, JSONC, Bash o CSS in sola lettura
tabPannelli etichettati contenenti blocchi annidati

Tipi di elemento

TypeDescription
buttonPulsante di azione con dialogo di conferma opzionale
linkNavigazione interna o esterna risolta dall’host
menuPulsante che apre un elenco di scelte; ogni scelta invia un’azione
text_inputInput di testo a una o più righe
number_inputInput numerico con min/max
selectSelezione a discesa
toggleInterruttore on/off
secret_inputInput mascherato per chiavi API e token
checkboxSelezionare più valori da un elenco fisso
comboboxSelezione a valore singolo con ricerca
date_inputValore data
radioScelta singola da un elenco di opzioni visibile

L’editor di campi Portable Text supporta anche repeater e media_picker. Non sono campi di modulo per una pagina di amministrazione di plugin sandbox.

Helper builder

Il pacchetto @emdash-cms/blocks esporta le stesse forme tramite gli oggetti builder blocks ed elements. I builder riducono gli errori sui nomi delle proprietà restituendo oggetti ordinari compatibili con 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" })]),
	],
};

Campi condizionali

I campi di modulo possono essere mostrati in modo condizionale in base ad altri valori di 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 }
}

Il campo api_key appare solo quando auth_enabled è attivato. Le condizioni sono valutate lato client senza round-trip.

secret_input usa has_value: true per indicare che esiste già un valore; non accetta né restituisce il valore memorizzato al caricamento della pagina. Il campo maschera la digitazione nel browser. Dichiara la chiave corrispondente come type: "secret" in admin.settingsSchema e salvala tramite ctx.settings affinché EmDash la cifri. Segui Secret settings prima di memorizzare le credenziali.

Provalo

Usa il Block Playground per costruire e testare layout di blocchi in modo interattivo.