Pannello di amministrazione

In questa pagina

Il pannello di amministrazione EmDash è un’applicazione React a pagina singola incorporata nel sito Astro. Offre un’interfaccia completa di gestione contenuti per editor e amministratori.

Panoramica dell’architettura

┌────────────────────────────────────────────────────────────────┐
│                    Shell Astro                                 │
│  /_emdash/admin/[...path].astro                              │
│                                                                │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                    SPA React                             │  │
│  │                                                          │  │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐   │  │
│  │  │  TanStack   │  │  TanStack   │  │     Kumo        │   │  │
│  │  │   Router    │  │   Query     │  │  Componenti     │   │  │
│  │  └─────────────┘  └─────────────┘  └─────────────────┘   │  │
│  │                                                          │  │
│  │  ┌────────────────────────────────────────────────────┐  │  │
│  │  │           Client API REST                          │  │  │
│  │  │           /_emdash/api/*                           │  │  │
│  │  └────────────────────────────────────────────────────┘  │  │
│  └──────────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────────┘

L’admin è un’app React «grande isola». Astro gestisce shell e autenticazione; navigazione e rendering nell’area admin avvengono lato client.

Stack tecnologico

LivelloTecnologiaScopo
RoutingTanStack RouterRouting lato client con tipi sicuri
DatiTanStack QueryStato server, cache, mutazioni
UIKumoComponenti accessibili (Base UI + Tailwind)
TabelleTanStack TableOrdinamento, filtri, paginazione
ModuliReact Hook Form + ZodValidazione allineata allo schema server
IconePhosphorIconografia coerente
EditorTipTapTesto ricco (Portable Text)

Struttura delle rotte

L’admin è montato su /_emdash/admin/ e usa il routing lato client:

PercorsoSchermata
/Dashboard
/content/:collectionElenco contenuti
/content/:collection/:idEditor contenuto
/content/:collection/newNuova voce
/mediaLibreria media
/content-typesBuilder schema (solo admin)
/menusMenu di navigazione
/widgetsAree widget
/taxonomiesGestione categorie/tag
/settingsImpostazioni sito
/plugins/:pluginId/*Pagine plugin

UI guidata dal manifest

L’admin non codifica a mano collezioni o plugin. Recupera un manifest dal server:

GET /_emdash/api/manifest

Risposta:

{
	"collections": [
		{
			"slug": "posts",
			"label": "Blog Posts",
			"labelSingular": "Post",
			"icon": "file-text",
			"supports": ["drafts", "revisions", "preview"],
			"fields": [
				{ "slug": "title", "type": "string", "required": true },
				{ "slug": "content", "type": "portableText" }
			]
		}
	],
	"plugins": [
		{
			"id": "audit-log",
			"label": "Audit Log",
			"adminPages": [{ "path": "history", "label": "Audit History" }],
			"widgets": [{ "id": "recent-activity", "title": "Recent Activity" }]
		}
	],
	"taxonomies": [{ "name": "category", "label": "Categories", "hierarchical": true }],
	"version": "abc123"
}

L’admin costruisce navigazione, moduli ed editor interamente da questo manifest. Vantaggi:

  • Le modifiche allo schema compaiono subito — Nessun rebuild dell’admin
  • L’UI dei plugin si integra automaticamente — Pagine e widget dal manifest
  • Sicurezza dei tipi al confine — Gli schema Zod restano sul server

Flusso dati

  1. Caricamento SPA admin — TanStack Router si inizializza 2. Fetch manifest — TanStack Query metadati collezioni/plugin in cache 3. Costruzione navigazione — Sidebar generata dal manifest 4. Navigazione utente — Routing lato client, nessun reload pagina 5. Fetch dati — TanStack Query interroga le API REST 6. Rendering moduli — Editor campo generati dai descrittori nel manifest 7. Invio modifiche — Mutazioni via TanStack Query, aggiornamenti ottimistici 8. Validazione server — Schema Zod sul server, errori in JSON

Endpoint API REST

L’admin comunica solo tramite API REST:

API contenuti

MetodoEndpointScopo
GET/api/content/:collectionElenco voci
POST/api/content/:collectionCrea voce
GET/api/content/:collection/:idOttieni voce
PUT/api/content/:collection/:idAggiorna voce
DELETE/api/content/:collection/:idEliminazione soft
GET/api/content/:collection/:id/revisionsElenco revisioni
POST/api/content/:collection/:id/preview-urlGenera URL anteprima

API schema

MetodoEndpointScopo
GET/api/schemaEsporta schema completo
GET/api/schema/collectionsElenco collezioni
POST/api/schema/collectionsCrea collezione
PUT/api/schema/collections/:slugAggiorna collezione
DELETE/api/schema/collections/:slugElimina collezione
POST/api/schema/collections/:slug/fieldsAggiungi campo
PUT/api/schema/collections/:slug/fields/:fieldAggiorna campo
DELETE/api/schema/collections/:slug/fields/:fieldElimina campo

API media

MetodoEndpointScopo
GET/api/mediaElenco media
POST/api/media/upload-urlURL firmato per upload
POST/api/media/:id/confirmConferma upload completato
DELETE/api/media/:idElimina media
GET/api/media/file/:keyServe file media

Altre API

EndpointScopo
/api/settingsImpostazioni sito (GET/POST)
/api/menus/*Menu navigazione
/api/widget-areas/*Gestione widget
/api/taxonomies/*Termini tassonomia
/api/admin/plugins/*Stato plugin

Paginazione

Tutti gli endpoint di elenco usano paginazione a cursore:

{
  "items": [...],
  "nextCursor": "eyJpZCI6IjAxSjEyMzQ1NiJ9"
}

Pagina successiva:

GET /api/content/posts?cursor=eyJpZCI6IjAxSjEyMzQ1NiJ9

UI admin dei plugin

I plugin possono estendere l’admin con pagine e widget della dashboard. L’integrazione genera un modulo virtuale con import statici:

// virtual:emdash/plugin-admins (generato)
import * as pluginAdmin0 from "@emdash-cms/plugin-seo/admin";
import * as pluginAdmin1 from "@emdash-cms/plugin-analytics/admin";

export const pluginAdmins = {
	seo: pluginAdmin0,
	analytics: pluginAdmin1,
};

Pagine plugin

Le pagine plugin sono montate sotto /_emdash/admin/plugins/:pluginId/*:

// @emdash-cms/plugin-seo/src/admin.tsx
export const pages = [
	{
		path: "settings",
		component: SEOSettingsPage,
		label: "SEO Settings",
	},
];

Renderizzata su: /_emdash/admin/plugins/seo/settings

Widget dashboard

I plugin possono aggiungere widget alla dashboard:

export const widgets = [
	{
		id: "seo-overview",
		component: SEOWidget,
		title: "SEO Overview",
		size: "half", // "full" | "half" | "third"
	},
];

Autenticazione

La rotta shell dell’admin applica l’autenticazione tramite middleware Astro:

// Logica middleware semplificata
export async function onRequest({ request, locals }, next) {
	const session = await getSession(request);

	if (request.url.includes("/_emdash/admin")) {
		if (!session?.user) {
			return redirect("/_emdash/admin/login");
		}
		locals.user = session.user;
	}

	return next();
}

La SPA admin non gestisce il login — è una pagina Astro che imposta il cookie di sessione.

Accesso basato sui ruoli

Ruoli diversi vedono sezioni diverse dell’admin:

RuoloSezioni visibili
EditorDashboard, collezioni assegnate, media
Admin+ Tipi di contenuto, tutte le collezioni, impostazioni
Developer+ Accesso CLI, tipi generati

L’endpoint manifest filtra collezioni e funzionalità in base al ruolo dell’utente che richiede.

Editor contenuti

L’editor genera moduli dinamicamente dalle definizioni dei campi:

// Rendering editor semplificato
function ContentEditor({ collection, fields }) {
	return (
		<form>
			{fields.map((field) => (
				<FieldWidget
					key={field.slug}
					type={field.type}
					label={field.label}
					required={field.required}
					options={field.options}
				/>
			))}
		</form>
	);
}

Ogni tipo di campo ha un widget corrispondente:

Tipo campoWidget
stringInput testo
textArea di testo
numberInput numerico
booleanInterruttore
datetimeSelettore data/ora
selectMenu a tendina
multiSelectSelezione multipla
portableTextEditor TipTap
imageSelettore media
referenceSelettore voce

Editor di testo ricco

I campi Portable Text usano TipTap (ProseMirror) per la modifica:

Utente digita → TipTap (JSON ProseMirror) → Salva → Portable Text (DB)
Carica → Portable Text (DB) → TipTap (JSON ProseMirror) → Visualizzazione

La conversione avviene ai confini caricamento/salvataggio con portableTextToProsemirror() e prosemirrorToPortableText().

Blocchi supportati:

  • Paragrafi, titoli (H1–H6)
  • Elenchi puntati e numerati
  • Citazioni, blocchi di codice
  • Immagini (dalla libreria media)
  • Link

Blocchi sconosciuti da plugin o import restano come segnaposto in sola lettura.

Libreria media

La libreria media offre:

  • Vista griglia ed elenco
  • Ricerca e filtro per tipo, data
  • Upload drag-and-drop
  • Anteprima immagine con metadati
  • Selezione ed eliminazione in blocco

Gli upload usano URL firmati per upload diretto client → storage:

  1. Richiesta URL uploadPOST /api/media/upload-url 2. Upload diretto — Il client fa PUT del file sull’URL firmato (R2/S3) 3. Conferma uploadPOST /api/media/:id/confirm 4. Il server estrae metadati — Dimensioni, tipo MIME, ecc.

Questo approccio evita i limiti di dimensione del body sui Workers e consente un avanzamento reale dell’upload.

Passi successivi