Questa pagina è per le persone che lavorano su EmDash, non per chi costruisce un sito con esso. Spiega il layout del database, l’integrazione Astro, i percorsi delle richieste, l’applicazione di amministrazione, il flusso dei media e il sistema di importazione. Se stai costruendo un sito, leggi invece Architettura e il Modello di contenuto.
L’integrazione Astro
EmDash viene eseguito come un’integrazione Astro dal pacchetto emdash. Al momento della build:
-
Inietta l’applicazione di amministrazione e le route API REST con l’API
injectRoutedi Astro. Nulla viene copiato nel progetto dell’utente. Le principali famiglie di route sono:Pattern del percorso Scopo /_emdash/admin/[...path]SPA del pannello di amministrazione /_emdash/api/manifestManifesto admin (collezioni, plugin) /_emdash/api/content/[collection]/...Operazioni sulle voci di contenuto /_emdash/api/media/...Operazioni della libreria media /_emdash/api/schema/...Gestione dello schema /_emdash/api/settings/...Impostazioni del sito /_emdash/api/menus/...Menu di navigazione /_emdash/api/taxonomies/...Categorie, tag, tassonomie personalizzate /_emdash/api/plugins/[pluginId]/[...path]Route API definite dai plugin L’iniettore di route è l’inventario completo, incluse autenticazione, commenti, ricerca, importazioni, widget e altre famiglie di route.
-
Genera moduli virtuali affinché il bundler possa risolvere il codice di configurazione ed estensione:
Modulo Scopo virtual:emdash/configConfigurazione database, storage e sito virtual:emdash/dialectFactory del dialetto database virtual:emdash/admin-registryImport statici per le interfacce admin dei plugin virtual:emdash/pluginsImplementazioni dei plugin configurati virtual:emdash/media-providersProvider di media esterni configurati virtual-modules.tsdefinisce gli helper runtime rimanenti e i contenuti dei moduli generati. -
Fornisce il loader Live Content Collections e registra il middleware runtime. Al momento della richiesta, il middleware apre le connessioni al database e allo storage configurate e applica eventuali migrazioni in sospeso prima che le route le utilizzino.
Schema database-first
Le definizioni dello schema risiedono nel database, non in un file di configurazione statico. _emdash_collections memorizza una riga per collezione. Le sue colonne principali descrivono la collezione e le funzionalità che il runtime e l’admin espongono:
| Colonne | Scopo |
|---|---|
id, slug | Identità stabile della collezione |
label, label_singular, description, icon | Nomi e indicazioni mostrati ai redattori |
supports, has_seo, comments_enabled, edit_locking | Capacità opzionali della collezione |
title_field, date_field, admin_config, hidden, sort_order | Comportamento della lista admin e della navigazione |
url_pattern, routable | URL pubblica e comportamento dello slug |
source | Come è stata creata la collezione |
Il valore source registra la provenienza come manual, seed, template:<name>, import:<name> o discovered. Le impostazioni aggiuntive provengono dalle migrazioni registrate, quindi database/types.ts e le migrazioni sono l’inventario attuale delle colonne.
_emdash_fields memorizza i campi collegati a ciascuna collezione:
| Colonne | Scopo |
|---|---|
id, collection_id, slug | Identità del campo e collezione proprietaria |
label, type, column_type | Etichetta dell’editor, tipo di campo EmDash e tipo di storage SQL |
required, unique, default_value, validation | Vincoli di contenuto e valori predefiniti |
widget, options, sort_order | Controllo dell’editor e ordine di visualizzazione |
searchable, indexed, translatable | Comportamento di ricerca, query e localizzazione |
collection_id fa riferimento a _emdash_collections.id, e ogni slug di campo è unico all’interno della sua collezione.
Tabelle di contenuto per collezione
Ogni collezione ottiene la propria tabella, con prefisso ec_. Una collezione products con campi title e price produce una tabella con questa forma:
CREATE TABLE ec_products (
-- Colonne di sistema, presenti in ogni tabella di contenuto
id TEXT PRIMARY KEY,
slug TEXT,
status TEXT DEFAULT 'draft',
author_id TEXT,
primary_byline_id TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
published_at TEXT,
scheduled_at TEXT,
deleted_at TEXT,
version INTEGER DEFAULT 1,
live_revision_id TEXT,
draft_revision_id TEXT,
locale TEXT NOT NULL DEFAULT 'en',
translation_group TEXT,
-- Colonne di contenuto, create dalle definizioni dei campi
title TEXT NOT NULL,
price REAL,
UNIQUE (slug, locale)
);
Le colonne reali danno a ciascun campo un tipo di database, consentono indici e chiavi esterne, e permettono agli strumenti di database di ispezionare lo schema senza decodificare un blob JSON di contenuto. Il vincolo unique consente alle traduzioni di condividere uno slug mantenendo ogni slug unico all’interno di una lingua. Tutte le varianti linguistiche della stessa voce condividono un valore translation_group, che permette a EmDash di trovare le righe che sono traduzioni l’una dell’altra.
Le principali preoccupazioni sui dati rimangono separate:
| Aspetto | Posizione | Tabelle |
|---|---|---|
| Schema | Tabelle di sistema | _emdash_collections, _emdash_fields |
| Contenuto | Tabelle per collezione | ec_posts, ec_products, … |
| Media | Tabella separata + storage | Tabella media + storage configurato |
| Impostazioni | Tabella opzioni | options con prefisso site: |
Modifiche dello schema a runtime
L’aggiunta di un campo tramite l’UI di amministrazione esegue questi passaggi:
- Inserire la definizione del campo in
_emdash_fields. - Aggiungere la colonna corrispondente alla tabella
ec_*della collezione e creare un indice quando il campo è configurato come indicizzato. - Aggiornare i tipi di sviluppo generati affinché il nuovo campo appaia negli strumenti dell’editor.
La validazione del contenuto legge le definizioni dei campi attuali e costruisce uno schema Zod quando il contenuto viene creato o aggiornato. La modifica del tipo SQL sottostante, del vincolo required o unique, o del comportamento di localizzazione di un campo può richiedere una migrazione manuale del contenuto; SchemaRegistry rifiuta le modifiche sul posto non supportate invece di ricostruire la tabella implicitamente.
Validazione a runtime
EmDash deriva uno schema Zod dai campi attuali della collezione. Il generatore delega i dettagli di tipo e vincolo a generateFieldSchema():
export function generateZodSchema(
collection: CollectionWithFields,
): z.ZodObject<Record<string, ZodType>> {
const shape: Record<string, ZodType> = {};
for (const field of collection.fields) {
shape[field.slug] = generateFieldSchema(field);
}
return z.object(shape);
}
Il gestore di contenuto rifiuta anche campi sconosciuti, verifica i valori stringa obbligatori e verifica i riferimenti ad altre collezioni.
Livello dati
EmDash utilizza Kysely per SQL tipizzato attraverso SQLite, libSQL, Cloudflare D1 e PostgreSQL. La configurazione del sito seleziona l’adattatore del database; l’integrazione espone la sua factory di dialetto tramite virtual:emdash/dialect.
Loader Live Content Collections
Il contenuto viene servito a runtime attraverso le Live Content Collections di Astro. emdashLoader() implementa l’interfaccia LiveLoader di Astro ed è registrato come una 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 collezione EmDash. getEmDashCollection("posts") fornisce il filtro di tipo posts, e il loader lo mappa alla tabella ec_posts.
Percorsi delle richieste
Una richiesta di contenuto da una pagina Astro segue questo percorso:
- La pagina chiama
getEmDashCollection()ogetEmDashEntry(). - Il wrapper di query chiama
getLiveCollection()ogetLiveEntry()di Astro con la collezione interna_emdashe il tipo di collezione EmDash richiesto. emdashLoader()interroga la tabellaec_*rilevante tramite Kysely, applicando regole di pubblicazione, lingua, filtro, ordinamento e paginazione.- Il wrapper di query mappa le righe alle voci Astro e carica le loro firme e i termini di tassonomia.
- Il componente Astro renderizza le voci restituite.
Lo stato di anteprima e modalità di modifica viaggia attraverso il contesto della richiesta, così le stesse funzioni di query possono restituire contenuto bozza dopo che il middleware ha verificato la richiesta.
Una richiesta API di amministrazione segue un percorso separato:
- Il middleware autentica la richiesta e memorizza l’utente risolto in
Astro.locals. - La route API analizza la richiesta e verifica il permesso necessario per quell’operazione.
- La route delega la logica di business a un gestore o repository.
- Il gestore esegue gli hook del ciclo di vita dei plugin attorno all’operazione di database quando quell’operazione espone hook.
- La route restituisce una risposta JSON standard di successo o errore all’applicazione di amministrazione.
Interni del pannello di amministrazione
L’admin è un’applicazione a pagina singola React. Astro serve il suo shell e il middleware di autenticazione protegge le route di amministrazione. All’interno dell’applicazione, TanStack Router gestisce la navigazione, TanStack Query carica lo stato del server, TanStack Table renderizza le griglie di dati, React Hook Form e Zod gestiscono i moduli, TipTap modifica il Portable Text e Kumo fornisce il sistema di design.
Per l’autenticazione di sessione, il middleware reindirizza una richiesta del browser non autenticata alla pagina di login e restituisce un errore JSON per una richiesta API non autenticata. Dopo aver caricato un utente attivo, lo posiziona su Astro.locals per la route:
const sessionUser = await resolveSessionUser(session);
if (!sessionUser?.id) {
if (isApiRoute) {
return apiError("NOT_AUTHENTICATED", "Not authenticated", 401);
}
const loginUrl = new URL("/_emdash/admin/login", getPublicOrigin(url, emdash?.config));
loginUrl.searchParams.set("redirect", url.pathname);
return context.redirect(loginUrl.toString());
}
Dopo questo ramo, il middleware carica l’utente, rifiuta gli account mancanti o disabilitati, posiziona l’utente attivo su Astro.locals e prosegue verso la route.
UI guidata dal manifesto
L’admin non codifica in modo rigido gli schemi delle collezioni o i contributi dei plugin. Recupera GET /_emdash/api/manifest, che descrive le collezioni attuali, i campi, i plugin, le tassonomie, la modalità di autenticazione e altre capacità configurate. Un manifesto abbreviato appare così:
{
"collections": {
"posts": {
"label": "Blog Posts",
"labelSingular": "Post",
"supports": ["drafts", "revisions", "preview"],
"fields": {
"title": { "kind": "string", "label": "Title", "required": true }
}
}
},
"plugins": {
"audit-log": { "version": "0.2.1", "enabled": true }
},
"taxonomies": [
{ "name": "category", "label": "Categories", "hierarchical": true }
],
"version": "0.37.0"
}
L’admin usa il manifesto per costruire la navigazione delle collezioni e gli editor dei campi. Poiché l’endpoint legge lo schema live, le modifiche a collezioni e campi appaiono senza ricostruire l’applicazione di amministrazione.
UI admin dei plugin
I punti di ingresso admin dei plugin configurati sono raccolti in virtual:emdash/admin-registry. Il modulo generato usa importazioni statiche affinché il bundler possa includere i componenti React:
import * as pluginAdmin0 from "@emdash-cms/plugin-seo/admin";
export const pluginAdmins = { seo: pluginAdmin0 };
Conversione del testo ricco
I campi Portable Text usano TipTap, che è basato su ProseMirror. EmDash converte il Portable Text in ProseMirror quando l’editor carica e lo riconverte in Portable Text quando la voce viene salvata. I blocchi sconosciuti da plugin o importazioni vengono preservati come segnaposto di sola lettura invece di essere scartati.
Upload firmati
Gli upload di media usano URL firmate dirette allo storage quando l’adattatore di storage le supporta e un endpoint di streaming same-origin altrimenti:
- Il client richiede una destinazione di upload da
POST /_emdash/api/media/upload-url. EmDash crea un elemento media in sospeso. - Il client carica verso la destinazione restituita. Gli adattatori compatibili con S3 possono restituire un URL firmato che aggira i limiti di dimensione del corpo dell’applicazione; i binding nativi R2 e lo storage locale restituiscono un endpoint di streaming EmDash.
- Il client conferma l’upload con
POST /_emdash/api/media/:id/confirm. - EmDash valida il file memorizzato e segna l’elemento media come pronto.
Estensione dell’importatore di contenuto
L’importatore WordPress usa un’interfaccia ImportSource estensibile. Una sorgente può sondare un URL, analizzare il contenuto disponibile rispetto allo schema attuale e trasmettere in streaming elementi di contenuto normalizzati:
interface ImportSource {
id: string;
name: string;
description: string;
icon: "upload" | "globe" | "wordpress" | "plug";
requiresFile?: boolean;
canProbe?: boolean;
probe?(url: string): Promise<SourceProbeResult | null>;
analyze(input: SourceInput, context: ImportContext): Promise<ImportAnalysis>;
fetchContent(input: SourceInput, options: FetchOptions): AsyncGenerator<NormalizedItem>;
fetchMedia?(url: string, input: SourceInput): Promise<Blob>;
}
La sorgente WXR importa file di esportazione WordPress. La sorgente connettore importa direttamente da siti con il plugin EmDash per WordPress. Una sorgente REST separata rileva siti WordPress pubblici, ma indirizza l’utente a un’esportazione WXR perché l’importazione REST diretta non è implementata. Registra un’altra sorgente quando un importatore può produrre le stesse forme di analisi normalizzata e elementi di contenuto.