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
| Livello | Tecnologia | Scopo |
|---|---|---|
| Routing | TanStack Router | Routing lato client con tipi sicuri |
| Dati | TanStack Query | Stato server, cache, mutazioni |
| UI | Kumo | Componenti accessibili (Base UI + Tailwind) |
| Tabelle | TanStack Table | Ordinamento, filtri, paginazione |
| Moduli | React Hook Form + Zod | Validazione allineata allo schema server |
| Icone | Phosphor | Iconografia coerente |
| Editor | TipTap | Testo ricco (Portable Text) |
Struttura delle rotte
L’admin è montato su /_emdash/admin/ e usa il routing lato client:
| Percorso | Schermata |
|---|---|
/ | Dashboard |
/content/:collection | Elenco contenuti |
/content/:collection/:id | Editor contenuto |
/content/:collection/new | Nuova voce |
/media | Libreria media |
/content-types | Builder schema (solo admin) |
/menus | Menu di navigazione |
/widgets | Aree widget |
/taxonomies | Gestione categorie/tag |
/settings | Impostazioni 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
- 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
| Metodo | Endpoint | Scopo |
|---|---|---|
GET | /api/content/:collection | Elenco voci |
POST | /api/content/:collection | Crea voce |
GET | /api/content/:collection/:id | Ottieni voce |
PUT | /api/content/:collection/:id | Aggiorna voce |
DELETE | /api/content/:collection/:id | Eliminazione soft |
GET | /api/content/:collection/:id/revisions | Elenco revisioni |
POST | /api/content/:collection/:id/preview-url | Genera URL anteprima |
API schema
| Metodo | Endpoint | Scopo |
|---|---|---|
GET | /api/schema | Esporta schema completo |
GET | /api/schema/collections | Elenco collezioni |
POST | /api/schema/collections | Crea collezione |
PUT | /api/schema/collections/:slug | Aggiorna collezione |
DELETE | /api/schema/collections/:slug | Elimina collezione |
POST | /api/schema/collections/:slug/fields | Aggiungi campo |
PUT | /api/schema/collections/:slug/fields/:field | Aggiorna campo |
DELETE | /api/schema/collections/:slug/fields/:field | Elimina campo |
API media
| Metodo | Endpoint | Scopo |
|---|---|---|
GET | /api/media | Elenco media |
POST | /api/media/upload-url | URL firmato per upload |
POST | /api/media/:id/confirm | Conferma upload completato |
DELETE | /api/media/:id | Elimina media |
GET | /api/media/file/:key | Serve file media |
Altre API
| Endpoint | Scopo |
|---|---|
/api/settings | Impostazioni 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:
| Ruolo | Sezioni visibili |
|---|---|
| Editor | Dashboard, 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 campo | Widget |
|---|---|
string | Input testo |
text | Area di testo |
number | Input numerico |
boolean | Interruttore |
datetime | Selettore data/ora |
select | Menu a tendina |
multiSelect | Selezione multipla |
portableText | Editor TipTap |
image | Selettore media |
reference | Selettore 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:
- Richiesta URL upload —
POST /api/media/upload-url2. Upload diretto — Il client fa PUT del file sull’URL firmato (R2/S3) 3. Conferma upload —POST /api/media/:id/confirm4. 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.