Questa pagina è per chi lavora su EmDash, non per chi sta costruendo un sito con esso. Documenta meccaniche interne: layout di tabelle, l’integrazione Astro, il percorso delle richieste, generazione di codice. Niente di tutto ciò è necessario per usare EmDash. Se stai costruendo un sito, leggi invece Architecture e il Content Model.
L’integrazione Astro
EmDash viene eseguito come integrazione Astro dal pacchetto emdash. Al momento della build:
-
Inietta l’SPA admin e le route API REST con l’API
injectRoutedi Astro. Nulla viene copiato nel progetto dell’utente. I percorsi iniettati sono:Pattern del percorso Scopo /_emdash/admin/[...path]SPA pannello amministrativo /_emdash/api/manifestManifest admin (collezioni, plugin) /_emdash/api/content/[collection]CRUD voci di contenuto /_emdash/api/media/*Operazioni libreria media /_emdash/api/schema/*Gestione schema /_emdash/api/settingsImpostazioni sito /_emdash/api/menus/*Menu di navigazione /_emdash/api/taxonomies/*Categorie, tag, tassonomie personalizzate -
Genera moduli virtuali in modo che il bundler possa risolvere e tree-shake il codice di configurazione e plugin:
Modulo Scopo virtual:emdash/configConfigurazione database e storage virtual:emdash/dialectFactory dialetto database virtual:emdash/plugin-adminsImportazioni statiche per UI admin plugin -
Fornisce il loader Live Collections, gestisce le migrazioni e apre la connessione di storage.
Schema database-first
Le definizioni di schema vivono nel database, non nel codice. Due tabelle di sistema tracciano la struttura.
_emdash_collections contiene una riga per collezione:
CREATE TABLE _emdash_collections (
id TEXT PRIMARY KEY,
slug TEXT UNIQUE NOT NULL, -- "posts", "products"
label TEXT NOT NULL, -- "Blog Posts"
label_singular TEXT, -- "Post"
description TEXT,
icon TEXT,
supports JSON, -- ["drafts", "revisions", "preview"]
source TEXT, -- how it was created
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT
);
La colonna source registra la provenienza: manual (UI admin), template:<name> (file seed), import:wordpress (importatore) o discovered (auto-rilevato da tabelle esistenti).
_emdash_fields contiene una riga per campo, collegata alla sua collezione:
CREATE TABLE _emdash_fields (
id TEXT PRIMARY KEY,
collection_id TEXT REFERENCES _emdash_collections(id),
slug TEXT NOT NULL, -- column name
label TEXT NOT NULL,
type TEXT NOT NULL, -- field type
column_type TEXT NOT NULL, -- TEXT, REAL, INTEGER, JSON
required INTEGER DEFAULT 0,
unique_field INTEGER DEFAULT 0,
default_value TEXT,
validation JSON,
widget TEXT,
options JSON,
sort_order INTEGER,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(collection_id, slug)
);
Tabelle di contenuto per collezione
Ogni collezione ottiene la propria tabella, con prefisso ec_. Una collezione products con campi title e price produce:
CREATE TABLE ec_products (
-- System columns, always present
id TEXT PRIMARY KEY,
slug TEXT UNIQUE,
status TEXT DEFAULT 'draft',
author_id TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
published_at TEXT,
deleted_at TEXT, -- soft delete
version INTEGER DEFAULT 1, -- optimistic locking
-- Content columns, from field definitions
title TEXT NOT NULL,
price REAL
);
Colonne reali (anziché una tabella con un blob JSON) forniscono indicizzazione appropriata, chiavi esterne funzionanti, uno schema che gli strumenti di database possono ispezionare e nessuna analisi JSON campo per campo.
Le preoccupazioni rimangono separate:
| Preoccupazione | Posizione | Tabelle |
|---|---|---|
| Schema | Tabelle di sistema | _emdash_collections, _emdash_fields |
| Contenuto | Tabelle per collezione | ec_posts, ec_products, … |
| Media | Tabella separata + storage | Tabella media + R2/S3 |
| Impostazioni | Tabella opzioni | options con prefisso site: |
Modifiche schema a runtime
Aggiungere un campo tramite l’UI admin esegue tre passaggi:
- Inserisce un record in
_emdash_fields. - Esegue
ALTER TABLE ec_<collection> ADD COLUMN <name> <TYPE>. - Rigenera lo schema Zod utilizzato per la validazione.
SQLite supporta aggiungi, rinomina ed elimina colonna (elimina richiede SQLite 3.35+) a runtime. La modifica del tipo di una colonna non è supportata in loco, quindi EmDash ricostruisce la tabella in modo trasparente: crea una nuova tabella, copia le righe, elimina la vecchia tabella, rinomina la nuova.
Validazione a runtime
EmDash costruisce schemi Zod dalle definizioni dei campi all’avvio e valida ogni creazione e aggiornamento contro di essi:
function buildSchema(fields: Field[]): ZodSchema {
const shape: Record<string, ZodType> = {};
for (const field of fields) {
let zodType = fieldTypeToZod(field.type);
if (field.required) zodType = zodType.required();
if (field.validation?.min !== undefined) zodType = zodType.min(field.validation.min);
shape[field.slug] = zodType;
}
return z.object(shape);
}
Livello dati
EmDash utilizza Kysely per SQL type-safe su tutti i database supportati (SQLite, libSQL, Cloudflare D1 e PostgreSQL). Il dialetto è selezionato da virtual:emdash/dialect dalla configurazione che il sito passa all’integrazione.
Loader Live Collections
Il contenuto viene servito a runtime tramite le Live Collections di Astro. emdashLoader() implementa l’interfaccia LiveLoader di Astro ed è registrato come singola collezione _emdash:
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({ loader: emdashLoader() }),
};
La singola collezione _emdash avvolge ogni tipo di contenuto; il loader filtra per tipo quando viene chiamato getEmDashCollection("posts").
Percorsi richiesta
Una richiesta di contenuto da una pagina:
- Astro riceve la richiesta ed esegue il componente pagina.
getEmDashCollection()chiamagetLiveCollection()di Astro.emdashLoaderinterroga la tabellaec_*rilevante tramite Kysely.- Le righe sono mappate al formato entry di Astro (
id,slug,data). - Il componente renderizza.
Una richiesta admin:
- Il middleware valida il token di sessione.
- La route API esegue CRUD tramite un repository.
- Gli hook del ciclo di vita si attivano (ad esempio
content:beforeSave). - Kysely esegue l’SQL.
- La route restituisce JSON all’SPA admin.
Internals pannello amministrativo
L’admin è un’isola React. Astro serve lo shell e applica l’autenticazione nel middleware; tutto all’interno è lato client, costruito su TanStack Router, TanStack Query, TanStack Table, React Hook Form + Zod, TipTap e Kumo (Base UI di Cloudflare + sistema di design Tailwind).
La route shell controlla l’accesso nel middleware:
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();
}
UI guidata da manifest
L’admin non hardcoda nulla sulle collezioni o plugin. Recupera GET /_emdash/api/manifest, che restituisce le collezioni, plugin e tassonomie a cui l’utente richiedente può accedere, filtrati per ruolo:
{
"collections": [
{
"slug": "posts",
"label": "Blog Posts",
"icon": "file-text",
"supports": ["drafts", "revisions", "preview"],
"fields": [{ "slug": "title", "type": "string", "required": true }]
}
],
"plugins": [{ "id": "audit-log", "label": "Audit Log" }],
"taxonomies": [{ "name": "category", "label": "Categories", "hierarchical": true }],
"version": "abc123"
}
Navigazione, form ed editor di campi sono generati da questo manifest, quindi le modifiche di schema e plugin appaiono senza ricostruzione dell’admin, e gli schemi Zod rimangono lato server.
UI admin plugin
I punti di ingresso admin dei plugin sono raccolti in un modulo virtuale generato di importazioni statiche in modo che il bundler possa risolverli e tree-shake:
import * as pluginAdmin0 from "@emdash-cms/plugin-seo/admin";
export const pluginAdmins = { seo: pluginAdmin0 };
Conversione rich text
I campi Portable Text vengono modificati in TipTap (ProseMirror). Il contenuto viene convertito ai confini di caricamento e salvataggio da portableTextToProsemirror() e prosemirrorToPortableText(). I blocchi sconosciuti da plugin o importazioni sono conservati come segnaposto di sola lettura.
Caricamenti firmati
I caricamenti media aggirano i limiti di dimensione del corpo Worker con URL firmati diretti allo storage:
- Il client richiede un URL di caricamento (
POST /api/media/upload-url). - Il client carica direttamente all’URL firmato (R2 o S3).
- Il client conferma (
POST /api/media/:id/confirm). - Il server estrae i metadati (dimensioni, tipo MIME).
Estendere l’importatore contenuti
L’importatore WordPress è costruito su un’interfaccia pluggable ImportSource. Una fonte personalizzata implementa probe, analyze e fetch:
interface ImportSource {
probe(input: ImportInput): Promise<ProbeResult>;
analyze(input: ImportInput): Promise<AnalysisResult>;
fetchContent(input: ImportInput): AsyncIterable<NormalizedEntry>;
}
probe valida l’input e riporta ciò che ha trovato, analyze mappa i tipi di post sorgente alle collezioni EmDash e segnala lacune nello schema, e fetchContent trasmette voci normalizzate che la pipeline di importazione scrive tramite gli stessi repository che l’admin utilizza. Le fonti integrate coprono WordPress WXR, WordPress.com e l’API REST WordPress; registra una fonte personalizzata per importare da un altro sistema.