Modello di contenuto

In questa pagina

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:

ValoreDescrizione
manualCreata dall’admin
template:blogDal seed di un template
import:wordpressImportata da WordPress
discoveredRilevata 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:

  1. Inserisce un record in _emdash_fields
  2. Esegue ALTER TABLE ec_<collezione> ADD COLUMN nome_colonna TIPO
  3. Rigenera lo schema Zod per la validazione

SQLite supporta queste operazioni ALTER TABLE a runtime:

OperazioneSupportata
Aggiungere colonna
Rinominare colonna
Rimuovere colonnaSì (SQLite 3.35+)
Cambiare tipo di colonnaNo (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:

AspettoPosizioneTabelle
SchemaTabelle di sistema_emdash_collections, _emdash_fields
ContenutoTabelle per collezioneec_posts, ec_products, ecc.
MediaTabella separata + storageTabella media + R2/S3
ImpostazioniTabella opzionioptions 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:

  1. Aprire Tipi di contenuto nel pannello admin
  2. Fare clic su Aggiungi collezione
  3. Definire i campi tramite il builder visuale
  4. 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

ApproccioPosizione schemaModifica a runtimeType safety
EmDashDatabaseSì (completa)Generata dal DB
WordPressCodice PHP + EAVLimitata (meta field)Nessuna
StrapiFile di codiceNo (richiede rebuild)Generata al build
SanityFile di codiceNo (lo schema deve essere distribuito)Integrata
DirectusDatabaseSì (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