EmDash usa un modello di contenuto orientato al database, in cui le definizioni di schema risiedono nel database, non nel codice. È una scelta di progetto fondamentale che consente modifiche allo schema a runtime e un flusso accessibile senza ruolo sviluppatore.
Schema come dati
I CMS tradizionali come Strapi o Keystatic richiedono di definire lo schema nel codice:
// Approccio tradizionale: schema nel codice
const posts = collection({
fields: {
title: text({ required: true }),
content: richText(),
},
});
EmDash memorizza le stesse informazioni nelle tabelle:
-- tabella _emdash_collections
INSERT INTO _emdash_collections (slug, label)
VALUES ('posts', 'Blog Posts');
-- tabella _emdash_fields
INSERT INTO _emdash_fields (collection_id, slug, type, required)
VALUES
('coll_abc', 'title', 'string', true),
('coll_abc', 'content', 'portableText', false);
Entrambi gli approcci definiscono la stessa struttura. La differenza è dove vive quella struttura e come può essere modificata.
Perché il database per primo?
Modifica a runtime
Creare e modificare tipi di contenuto senza cambi al codice né rebuild. Le persone non-developer possono progettare il modello di dati dall’UI di amministrazione.
Colonne SQL reali
A differenza del modello EAV (Entity-Attribute-Value) di WordPress, ogni campo ha una colonna reale. Indici, chiavi esterne e ottimizzazione delle query.
Autodocumentato
Gli strumenti di database possono ispezionare lo schema direttamente. Non serve analizzare il codice per comprendere il modello dati.
Percorso di migrazione
Esportare lo schema come JSON per il controllo versione. Importarlo in nuovi ambienti.
Tabelle di schema
Due tabelle di sistema definiscono la struttura dei contenuti:
Tabella collezioni
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, -- Nome icona Lucide
supports JSON, -- ["drafts", "revisions", "preview"]
source TEXT, -- Come è stata creata
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT
);
Il campo source indica come è stata creata la collezione:
| Valore | Descrizione |
|---|---|
manual | Creata dall’admin |
template:blog | Dal seed di un template |
import:wordpress | Importata da WordPress |
discovered | Rilevata automaticamente da dati esistenti |
Tabella campi
CREATE TABLE _emdash_fields (
id TEXT PRIMARY KEY,
collection_id TEXT REFERENCES _emdash_collections(id),
slug TEXT NOT NULL, -- Nome colonna: "title", "price"
label TEXT NOT NULL, -- Etichetta visualizzata
type TEXT NOT NULL, -- Tipo di campo
column_type TEXT NOT NULL, -- Tipo SQLite: TEXT, REAL, INTEGER, JSON
required INTEGER DEFAULT 0,
unique_field INTEGER DEFAULT 0,
default_value TEXT, -- Valore predefinito codificato in JSON
validation JSON, -- Regole di validazione
widget TEXT, -- Identificativo widget personalizzato
options JSON, -- Opzioni widget
sort_order INTEGER,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(collection_id, slug)
);
Tabelle di contenuto
Ogni collezione ha una tabella con il prefisso ec_. Quando crei una collezione “products” con campi titolo e prezzo:
CREATE TABLE ec_products (
-- Colonne di sistema (sempre presenti)
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, -- Eliminazione logica
version INTEGER DEFAULT 1, -- Blocco ottimistico
-- Colonne di contenuto (dalle definizioni dei campi)
title TEXT NOT NULL,
price REAL
);
Modifiche allo schema a runtime
Quando aggiungi un campo dall’admin, EmDash:
- Inserisce un record in
_emdash_fields - Esegue
ALTER TABLE ec_<collezione> ADD COLUMN nome_colonna TIPO - Rigenera lo schema Zod per la validazione
SQLite supporta queste operazioni ALTER TABLE a runtime:
| Operazione | Supportata |
|---|---|
| Aggiungere colonna | Sì |
| Rinominare colonna | Sì |
| Rimuovere colonna | Sì (SQLite 3.35+) |
| Cambiare tipo di colonna | No (ricostruzione tabella necessaria) |
Per i cambi di tipo, EmDash gestisce la ricostruzione della tabella in modo trasparente: crea nuova tabella → copia dati → elimina tabella vecchia → rinomina tabella nuova.
Separazione schema / contenuto
EmDash mantiene una separazione netta:
| Aspetto | Posizione | Tabelle |
|---|---|---|
| Schema | Tabelle di sistema | _emdash_collections, _emdash_fields |
| Contenuto | Tabelle per collezione | ec_posts, ec_products, ecc. |
| Media | Tabella separata + storage | Tabella media + R2/S3 |
| Impostazioni | Tabella opzioni | options con prefisso site: |
Questa separazione significa:
- Lo schema può essere esportato senza i contenuti
- I contenuti possono essere migrati tra schemi diversi
- Le tabelle di sistema non vengono mai ingombrate con dati utente
Validazione a runtime
EmDash costruisce schemi Zod dalle definizioni dei campi nel database all’avvio:
// Esempio semplificato
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);
}
I contenuti vengono validati rispetto a questi schemi runtime su ogni operazione di creazione e aggiornamento.
Integrazione TypeScript
Genera tipi TypeScript dallo schema del database:
# Recupera lo schema dal database, genera i tipi
npx emdash types
Genera .emdash/types.ts:
// .emdash/types.ts (generato)
export interface Post {
title: string;
content: PortableTextBlock[];
excerpt?: string;
featuredImage?: string;
}
export interface Product {
title: string;
price: number;
quantity: number;
}
// Overload tipizzati per le funzioni di query
declare module "emdash" {
export function getEmDashCollection(
type: "posts",
): Promise<{ entries: ContentEntry<Post>[]; error?: Error }>;
export function getEmDashEntry(
type: "products",
id: string,
): Promise<{ entry: ContentEntry<Product> | null; error?: Error; isPreview: boolean }>;
}
Flussi sviluppatore / non sviluppatore
Gli sviluppatori possono usare la CLI:
# Recupera schema, genera tipi
npx emdash types
# Esporta schema come JSON
npx emdash export-seed > seed.json
Le persone non-sviluppatore usano esclusivamente l’admin:
- Aprire Tipi di contenuto nel pannello admin
- Fare clic su Aggiungi collezione
- Definire i campi tramite il builder visuale
- Iniziare a creare contenuti immediatamente
Entrambi gli approcci modificano le stesse tabelle del database.
File seed
I template e le esportazioni usano file seed JSON per definizioni di schema portabili:
{
"version": "1",
"collections": [
{
"slug": "posts",
"label": "Blog Posts",
"labelSingular": "Post",
"supports": ["drafts", "revisions", "preview"],
"fields": [
{ "slug": "title", "type": "string", "required": true },
{ "slug": "content", "type": "portableText" },
{ "slug": "featuredImage", "type": "image" }
]
}
],
"taxonomies": [{ "name": "category", "label": "Categories", "hierarchical": true }],
"menus": [{ "name": "primary", "label": "Primary Navigation" }]
}
Applica i file seed in modo programmatico:
import { applySeed, validateSeed } from "emdash/seed";
import seedData from "./.emdash/seed.json";
// Valida prima
const { valid, errors } = validateSeed(seedData);
// Applica (idempotente — sicuro da rieseguire)
await applySeed(db, seedData, {
includeContent: true,
onConflict: "skip", // 'skip' | 'update' | 'error'
});
Confronto con altri approcci
| Approccio | Posizione schema | Modifica a runtime | Type safety |
|---|---|---|---|
| EmDash | Database | Sì (completa) | Generata dal DB |
| WordPress | Codice PHP + EAV | Limitata (meta field) | Nessuna |
| Strapi | File di codice | No (richiede rebuild) | Generata al build |
| Sanity | File di codice | No (lo schema deve essere distribuito) | Integrata |
| Directus | Database | Sì (completa) | Generata dal DB |
EmDash segue il modello Directus: database-first con generazione opzionale dei tipi. Questo offre la massima flessibilità pur supportando lo sviluppo type-safe quando desiderato.
Passi successivi
Collezioni
Scopri i tipi di campo e la validazione.
Pannello admin
Esplora l’architettura dell’admin.
Seeding
Configura i siti con i file seed.