EmDash wird über zwei Dateien konfiguriert: astro.config.mjs für die Integration und src/live.config.ts für Content Collections.
Astro-Integration
Konfigurieren Sie EmDash als Astro-Integration in astro.config.mjs:
import { defineConfig } from "astro/config";
import emdash, { local, s3 } from "emdash/astro";
import { sqlite, libsql } from "emdash/db";
export default defineConfig({
integrations: [
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
plugins: [],
}),
],
});
Integrationsoptionen
database
Erforderlich. Datenbank-Adapter-Konfiguration. Wählen Sie einen Adapter:
// SQLite (Node.js)
database: sqlite({ url: "file:./data.db" });
// PostgreSQL
database: postgres({ connectionString: process.env.DATABASE_URL });
// libSQL
database: libsql({
url: process.env.LIBSQL_DATABASE_URL,
authToken: process.env.LIBSQL_AUTH_TOKEN,
});
// Cloudflare D1 (Import aus @emdash-cms/cloudflare)
database: d1({ binding: "DB" });
Siehe Datenbankoptionen für Details.
storage
Erforderlich. Medienspeicher-Adapter-Konfiguration. Wählen Sie einen Adapter:
// Lokales Dateisystem (Entwicklung)
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
});
// R2-Bindung (Cloudflare Workers)
storage: r2({
binding: "MEDIA",
publicUrl: "https://pub-xxxx.r2.dev", // optional
});
// S3-kompatibel (beliebige Plattform) — alle Felder aus S3_*-Umgebungsvariablen
storage: s3()
// Oder mit expliziten Werten
storage: s3({
endpoint: "https://s3.amazonaws.com",
bucket: "my-bucket",
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
region: "us-east-1", // optional, Standard: "auto"
publicUrl: "https://cdn.example.com", // optional
});
Siehe Speicheroptionen für Details.
plugins
Optional. Array von EmDash-Plugins. Das folgende Beispiel registriert ein Plugin:
import seoPlugin from "@emdash-cms/plugin-seo";
plugins: [seoPlugin()];
fonts
Optional. Admin-UI-Schriftart-Konfiguration.
Standardmäßig lädt EmDash Noto Sans über die Astro Font API. Schriftarten werden zur Build-Zeit von Google heruntergeladen und selbst gehostet, sodass keine Laufzeit-CDN-Anfragen erfolgen. Die Basisschrift deckt lateinische, kyrillische, griechische, Devanagari- und vietnamesische Schriften ab.
Um Unterstützung für zusätzliche Schriftsysteme hinzuzufügen, übergeben Sie Schriftnamen. Das folgende Beispiel fügt Arabisch und Japanisch hinzu:
emdash({
fonts: {
scripts: ["arabic", "japanese"],
},
})
Die verfügbaren Schriften sind arabic, armenian, bengali, chinese-simplified, chinese-traditional, chinese-hongkong, devanagari, ethiopic, farsi, georgian, gujarati, gurmukhi, hebrew, japanese, kannada, khmer, korean, lao, malayalam, myanmar, oriya, sinhala, tamil, telugu, thai und tibetan.
Jede Schrift wird auf die entsprechende Noto Sans-Variante in Google Fonts abgebildet (z. B. lädt "arabic" Noto Sans Arabic). Alle Schriftarten teilen einen einzigen font-family-Namen und verwenden unicode-range, sodass der Browser nur die Dateien herunterlädt, die für die Zeichen auf der Seite benötigt werden.
Setzen Sie auf false, um die Schrifteinbindung vollständig zu deaktivieren und Systemschriften zu verwenden:
emdash({
fonts: false,
})
Das Admin-CSS verwendet die CSS-Variable --font-emdash. Diese wird automatisch durch die obige Schriftkonfiguration gesetzt.
auth
Optional. Ein Authentifizierungsadapter. EmDashs integrierter Login verwendet Passkeys; das Setzen von auth ersetzt sie durch einen externen Anbieter. Der Cloudflare Access-Adapter access() wird von @emdash-cms/cloudflare bereitgestellt:
import { access } from "@emdash-cms/cloudflare";
emdash({
auth: access({
teamDomain: "myteam.cloudflareaccess.com",
audience: "your-app-audience-tag",
roleMapping: {
Admins: 50,
Editors: 40,
},
}),
});
Optionen für access():
| Option | Typ | Standard | Beschreibung |
|---|---|---|---|
teamDomain | string | erforderlich | Ihre Cloudflare Access Team-Domain |
audience | string | — | Application Audience (AUD)-Tag. Bei Workers bevorzugen Sie audienceEnvVar. |
audienceEnvVar | string | "CF_ACCESS_AUDIENCE" | Umgebungsvariable, aus der das Audience-Tag zur Laufzeit gelesen wird |
autoProvision | boolean | true | Erstellt einen EmDash-Benutzer beim ersten Login |
defaultRole | number | 30 | Rollenstufe für Benutzer, die nicht durch roleMapping abgeglichen werden (siehe Benutzerrollen) |
syncRoles | boolean | false | Wendet roleMapping bei jedem Login erneut an, anstatt nur bei der Bereitstellung |
roleMapping | object | — | Ordnet IdP-Gruppennamen EmDash-Rollenstufen zu; erste Übereinstimmung gewinnt |
authProviders
Optional. Ein Array von pluggbaren Login-Providern (auf oberster Ebene, neben auth). Jeder Eintrag ist das Ergebnis des Aufrufs einer Provider-Factory, wie unten gezeigt:
import { github } from "emdash/auth/providers/github";
import { google } from "emdash/auth/providers/google";
import { atproto } from "@emdash-cms/auth-atproto";
emdash({
authProviders: [github(), google(), atproto()],
});
Integrierte Provider:
github()— liestEMDASH_OAUTH_GITHUB_CLIENT_ID/EMDASH_OAUTH_GITHUB_CLIENT_SECRET(oder unpräfixierte Fallbacks).google()— liestEMDASH_OAUTH_GOOGLE_CLIENT_ID/EMDASH_OAUTH_GOOGLE_CLIENT_SECRET.atproto()— Atmosphere-Konto-Login (Bluesky und das breitere AT-Protocol-Netzwerk). Keine Umgebungsvariablen erforderlich. Akzeptiert{ allowedDIDs, allowedHandles, defaultRole }. Siehe den Atmosphere-Login-Leitfaden.
Drittanbieter-Pakete können ihre eigenen Provider mit derselben AuthProviderDescriptor-Form registrieren — siehe Login-Provider.
siteUrl
Optional. Der öffentliche, dem Browser zugewandte Origin für die Website (Schema + Host + optionaler Port, kein Pfad).
Hinter einem TLS-terminierenden Reverse-Proxy gibt Astro.url die interne Adresse (http://localhost:4321) anstelle der öffentlichen (https://cms.example.com) zurück. Dies bricht Passkeys, CSRF-Origin-Abgleich, OAuth-Weiterleitungen, Login-Weiterleitungen, MCP-Discovery, Snapshot-Exporte, Sitemap, robots.txt und JSON-LD-strukturierte Daten. Setzen Sie siteUrl, um all dies auf einmal zu beheben.
Die Integration validiert diesen Wert zur Ladezeit: Er muss eine gültige URL mit http:- oder https:-Protokoll sein und wird auf Origin normalisiert (Pfad wird entfernt).
Das folgende Beispiel setzt den öffentlichen Origin:
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
siteUrl: "https://cms.example.com",
});
Wenn siteUrl nicht in der Konfiguration gesetzt ist, prüft EmDash Umgebungsvariablen in dieser Reihenfolge: EMDASH_SITE_URL, dann SITE_URL. Dies ist nützlich für Container-Deployments, bei denen die öffentliche URL zur Laufzeit gesetzt wird.
Multi-Origin-Passkey-Verifizierung
siteUrl definiert einen einzigen kanonischen Origin. Wenn dieselbe EmDash-Bereitstellung unter mehreren Hostnamen erreichbar ist, die eine registrierbare Elterndomäne teilen (z. B. https://example.com und https://preview.example.com), lehnt die Passkey-Verifizierung Assertions ab, deren Origin nicht exakt mit siteUrl übereinstimmt — obwohl WebAuthn Passkeys über Subdomänen unter derselben rpId als gültig erlaubt.
Deklarieren Sie zusätzliche akzeptierte Origins entweder über allowedOrigins in astro.config.mjs oder die EMDASH_ALLOWED_ORIGINS-Umgebungsvariable. Der kanonische siteUrl bleibt die Quelle der rpId; hier aufgeführte Einträge werden zur Verifizierungszeit akzeptiert. Die beiden Quellen werden zur Laufzeit zusammengeführt, sodass die Konfiguration stabile Origins deklarieren kann (versioniert, code-reviewed), während die Umgebung umgebungsspezifische Extras hinzufügt (z. B. ephemere PR-Vorschauen).
Das folgende Beispiel deklariert einen zusätzlichen Origin in der Konfiguration:
emdash({
siteUrl: "https://example.com",
allowedOrigins: ["https://preview.example.com"],
})
Die äquivalenten Werte können auch aus Umgebungsvariablen stammen:
EMDASH_SITE_URL=https://example.com
EMDASH_ALLOWED_ORIGINS=https://preview.example.com,https://staging.example.com
Validierung
EmDash validiert diese, um tote Konfiguration zu verhindern, die der Browser niemals akzeptieren würde:
- Jeder Eintrag muss eine parsbare
http:- oderhttps:-URL ohne abschließenden Punkt und ohne leere Labels im Hostnamen sein. - Wenn
allowedOriginsnicht leer ist, musssiteUrlgesetzt sein (aus einer der beiden Quellen) und darf kein IP-Literal sein oder einen Hostnamen mit abschließendem Punkt haben. - Jeder Origin muss derselbe Hostname wie
siteUrloder eine Subdomain davon sein. (WebAuthn erfordert, dassrpIdein registrierbares Suffix jedes Origins ist.)
Wenn die Validierung fehlschlägt, sehen Sie einen quellenattribuierten Fehler wie EmDash config error in EMDASH_ALLOWED_ORIGINS: "https://other-site.com" is not a subdomain of siteUrl "https://example.com". Allowed origins must be the same hostname as siteUrl or a subdomain of it.
Wo der Fehler auftritt, hängt davon ab, wo die Werte deklariert sind:
- Beim Astro-Start, wenn sowohl
config.allowedOriginsals auchconfig.siteUrlausastro.config.mjsstammen — Tippfehler im Code lassen den Build fehlschlagen. - Bei der ersten Passkey-Verifizierung, wenn einer der Werte aus
EMDASH_ALLOWED_ORIGINSoderEMDASH_SITE_URLstammt — Umgebungsmismatches treten als 500er beim ersten Verifizierungsversuch auf.
Reverse-Proxy-Einrichtung
Astro reflektiert X-Forwarded-* nur, wenn der öffentliche Host erlaubt ist. Konfigurieren Sie security.allowedDomains für den Hostnamen (und Schemata), den Ihre Benutzer treffen. In astro dev fügen Sie passende vite.server.allowedHosts hinzu, damit Vite den Proxy-Host-Header akzeptiert.
Bevorzugen Sie zuerst die Behebung von allowedDomains (und weitergeleiteten Headern); verwenden Sie siteUrl, wenn die rekonstruierte URL immer noch vom Browser-Origin abweicht (typisch, wenn TLS vorne terminiert wird und die Upstream-Anfrage http:// bleibt).
Mit TLS vorne reicht oft die Bindung des Dev-Servers an Loopback (astro dev --host 127.0.0.1) aus: Der Proxy verbindet sich lokal, während siteUrl mit dem öffentlichen HTTPS-Origin übereinstimmt.
Wenn Ihr Proxy einen Client-IP-Header schreibt, setzen Sie trustedProxyHeaders, damit EmDashs Ratenbegrenzungen die echte Client-IP verwenden können, anstatt jede Anfrage unter einem gemeinsamen “unknown”-Schlüssel zu gruppieren.
Die folgende Konfiguration setzt allowedDomains, vite.server.allowedHosts und siteUrl zusammen für eine Reverse-Proxy-Bereitstellung:
import { defineConfig } from "astro/config";
import emdash, { local } from "emdash/astro";
import { sqlite } from "emdash/db";
export default defineConfig({
security: {
allowedDomains: [
{ hostname: "cms.example.com", protocol: "https" },
{ hostname: "cms.example.com", protocol: "http" },
],
},
vite: {
server: {
allowedHosts: ["cms.example.com"],
},
},
integrations: [
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
siteUrl: "https://cms.example.com",
}),
],
});
trustedProxyHeaders
Optional. Header, denen bei der Client-IP-Auflösung vertraut werden soll, wenn hinter einem von Ihnen kontrollierten Reverse-Proxy läuft. Wird von Auth-Ratenbegrenzungen (Magic-Link, Registrierung, Passkey, OAuth-Device-Flow) und dem öffentlichen Kommentar-Endpunkt verwendet.
Bei Cloudflare wird das an die Anfrage angehängte cf-Objekt automatisch verwendet — Sie müssen dies normalerweise nicht setzen. Bei selbst gehosteten Bereitstellungen hinter nginx, Caddy, Traefik, Fly, Railway oder ähnlichen setzen Sie dies auf den Header, den Ihr Proxy schreibt, damit Ratenbegrenzungen nach echter Client-IP gruppieren können, anstatt jede Anfrage als “unknown” zu behandeln.
Das folgende Beispiel vertraut dem x-real-ip-Header, der von nginx, Caddy oder Traefik gesetzt wird:
emdash({
database: sqlite({ url: "file:./data.db" }),
trustedProxyHeaders: ["x-real-ip"],
});
Header werden in Reihenfolge versucht. Werte, die auf *-forwarded-for passen, werden als kommagetrennte Listen geparst und der erste Eintrag wird verwendet. Das folgende Beispiel bevorzugt Fly.ios Header und fällt auf x-forwarded-for zurück:
emdash({
trustedProxyHeaders: ["fly-client-ip", "x-forwarded-for"],
});
Wenn nicht in der Konfiguration gesetzt, liest EmDash die EMDASH_TRUSTED_PROXY_HEADERS-Umgebungsvariable (kommagetrennt). Ein explizites leeres Array in der Konfiguration überschreibt die Umgebungsvariable.
maxUploadSize
Optional. Maximal zulässige Mediendatei-Upload-Größe in Bytes. Gilt sowohl für direkte Multipart-Uploads als auch für signierte URL-Uploads. Standardwert ist 52_428_800 (50 MB). Das folgende Beispiel erhöht das Limit auf 100 MB:
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
maxUploadSize: 100 * 1024 * 1024, // 100 MB
});
| Wert | Beschreibung |
|---|---|
number (Bytes) | Muss eine positive endliche Ganzzahl sein |
| weggelassen | Standardwert ist 50 MB |
Uploads, die das konfigurierte Limit überschreiten, werden mit einer 413 Payload Too Large-Antwort auf dem direkten Upload-Pfad oder einem 400 Validation Error auf dem signierten URL-Pfad abgelehnt.
Datenbank-Adapter
Importieren Sie die Adapter aus emdash/db:
import { sqlite, libsql, postgres } from "emdash/db";
sqlite(config)
SQLite-Datenbank mit better-sqlite3. Das folgende Beispiel verbindet sich mit einer lokalen Datei:
| Option | Typ | Beschreibung |
|---|---|---|
url | string | Dateipfad mit file:-Präfix |
sqlite({ url: "file:./data.db" });
libsql(config)
libSQL-Datenbank. Das folgende Beispiel verbindet sich mit einer entfernten libSQL-Datenbank:
| Option | Typ | Beschreibung |
|---|---|---|
url | string | Datenbank-URL |
authToken | string | Auth-Token (optional für lokale Dateien) |
libsql({
url: process.env.LIBSQL_DATABASE_URL,
authToken: process.env.LIBSQL_AUTH_TOKEN,
});
postgres(config)
PostgreSQL-Datenbank mit Connection-Pooling.
| Option | Typ | Beschreibung |
|---|---|---|
connectionString | string | PostgreSQL-Verbindungs-URL |
host | string | Datenbank-Host |
port | number | Datenbank-Port |
database | string | Datenbankname |
user | string | Datenbankbenutzer |
password | string | Datenbankpasswort |
ssl | boolean | SSL aktivieren |
pool.min | number | Minimale Pool-Größe (Standard: 0) |
pool.max | number | Maximale Pool-Größe (Standard: 10) |
Das folgende Beispiel verbindet sich mit einem Connection-String:
postgres({ connectionString: process.env.DATABASE_URL });
d1(config)
Cloudflare D1-Datenbank. Import aus @emdash-cms/cloudflare.
| Option | Typ | Standard | Beschreibung |
|---|---|---|---|
binding | string | — | D1-Bindungsname aus wrangler.jsonc |
session | string | "disabled" | Lesereplikationsmodus: "disabled", "auto" oder "primary-first" |
bookmarkCookie | string | "__em_d1_bookmark" | Cookie-Name für Session-Bookmarks |
Das folgende Beispiel zeigt eine Basisbindung und eine mit aktivierten Lesereplikaten:
// Basis
d1({ binding: "DB" });
// Mit Lesereplikaten
d1({ binding: "DB", session: "auto" });
Wenn session "auto" oder "primary-first" ist, verwendet EmDash die D1 Sessions API, um Leseabfragen an nahe Replikate zu routen. Authentifizierte Benutzer erhalten Bookmark-basierte Read-your-writes-Konsistenz. Siehe Datenbankoptionen — Lesereplikate für Details.
Speicher-Adapter
Importieren Sie local und s3 aus emdash/astro. Der r2-Adapter wird aus @emdash-cms/cloudflare importiert:
import emdash, { local, s3 } from "emdash/astro";
import { r2 } from "@emdash-cms/cloudflare";
local(config)
Lokaler Dateisystem-Speicher. Das folgende Beispiel serviert Uploads aus einem lokalen Verzeichnis:
| Option | Typ | Beschreibung |
|---|---|---|
directory | string | Verzeichnispfad |
baseUrl | string | Basis-URL zum Servieren von Dateien |
local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
});
r2(config)
Cloudflare R2-Bindung. Das folgende Beispiel verwendet eine R2-Bindung mit einer öffentlichen URL:
| Option | Typ | Beschreibung |
|---|---|---|
binding | string | R2-Bindungsname |
publicUrl | string | Optionale öffentliche URL |
r2({
binding: "MEDIA",
publicUrl: "https://pub-xxxx.r2.dev",
});
s3(config?)
S3-kompatibler Speicher. Alle Konfigurationsfelder sind optional: Jedes Feld, das in
s3({...}) weggelassen wird, wird aus der entsprechenden S3_*-Umgebungsvariable aufgelöst, wenn der
Node-Prozess startet. Explizite Werte haben immer Vorrang.
Voraussetzung: Installieren Sie @aws-sdk/client-s3 und @aws-sdk/s3-request-presigner
in Ihrem Projekt. EmDash Core bündelt das AWS SDK nicht. Siehe
Speicheroptionen: S3-kompatibler Speicher
für Details.
| Option | Typ | Beschreibung |
|---|---|---|
endpoint | string | S3-Endpunkt-URL (S3_ENDPOINT) |
bucket | string | Bucket-Name (S3_BUCKET) |
accessKeyId | string | Zugriffsschlüssel (S3_ACCESS_KEY_ID) |
secretAccessKey | string | Geheimer Schlüssel (S3_SECRET_ACCESS_KEY) |
region | string | Region, Standard "auto" (S3_REGION) |
publicUrl | string | Optionale CDN-URL (S3_PUBLIC_URL) |
Die folgenden Beispiele lösen alle Felder aus der Umgebung auf, mischen Konfiguration und Umgebung oder übergeben jedes Feld explizit:
// Alle Felder aus S3_*-Umgebungsvariablen (Node-Container-Bereitstellungen)
s3()
// Mix: CDN aus Konfiguration, Rest aus Umgebung
s3({ publicUrl: "https://cdn.example.com" })
// Alles explizit
s3({
endpoint: "https://xxx.r2.cloudflarestorage.com",
bucket: "media",
accessKeyId: process.env.R2_ACCESS_KEY_ID,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
publicUrl: "https://cdn.example.com",
})
Laufzeit-Umgebungsvariablen-Auflösung ist ein Node-only-Feature. Bei Cloudflare
Workers werden Secrets und Variablen über den env-Parameter des
Fetch-Handlers bereitgestellt, nicht über process.env, sodass S3_*-Umgebungsvariablen
nicht erfasst werden. Workers-Bereitstellungen sollten entweder den r2(config)-Adapter verwenden
oder explizite Werte an s3({...}) übergeben. Siehe
Speicheroptionen für Details.
Live-Collections
Konfigurieren Sie den EmDash-Loader in src/live.config.ts:
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({
loader: emdashLoader(),
}),
};
Loader-Optionen
Die Funktion emdashLoader() nimmt keine Argumente:
emdashLoader();
Umgebungsvariablen
EmDash respektiert diese Umgebungsvariablen:
| Variable | Beschreibung |
|---|---|
EMDASH_SITE_URL | Öffentlicher Browser-zugewandter Origin (fällt auf SITE_URL zurück) |
EMDASH_ALLOWED_ORIGINS | Kommagetrennte Liste zusätzlicher Origins, die von der Passkey-Verifizierung akzeptiert werden (Multi-Subdomain-Bereitstellungen). |
EMDASH_DATABASE_URL | Datenbank-URL überschreiben |
EMDASH_ENCRYPTION_KEY | Schlüssel zum Verschlüsseln von Plugin-Secrets im Ruhezustand. Vom Betreiber bereitgestellt — niemals in der Datenbank gespeichert. |
EMDASH_PREVIEW_SECRET | Optionale Überschreibung für Preview-HMAC-Secret. Wenn nicht gesetzt, wird ein stabiler pro-Site-Wert generiert und in der Datenbank gespeichert. |
EMDASH_IP_SALT | Optionale Überschreibung für das Commenter-IP-Hash-Salt. Wenn nicht gesetzt, wird ein stabiler pro-Site-Wert generiert und in der Datenbank gespeichert. |
EMDASH_AUTH_SECRET | Legacy. Wird als IP-Salt-Quelle verwendet, wenn gesetzt; bestehende Installationen sollten dies beibehalten, um stabile Commenter-IP-Hashes über Upgrades hinweg zu bewahren. |
EMDASH_URL | Entfernte EmDash-URL für Schema-Sync |
Generieren Sie einen Verschlüsselungsschlüssel mit dem folgenden Befehl:
npx emdash secrets generate
package.json-Konfiguration
Templates und Sites können optionale Metadaten unter einem emdash-Schlüssel in package.json deklarieren:
{
"emdash": {
"label": "My Blog Template",
"seed": ".emdash/seed.json",
"url": "https://my-site.pages.dev"
}
}
| Option | Beschreibung |
|---|---|
label | Vorlagenname zur Anzeige |
seed | Pfad zur Seed-JSON-Datei |
url | Entfernte URL für Schema-Sync |
TypeScript-Konfiguration
EmDash generiert Typen in .emdash/types.ts. Fügen Sie einen Pfad-Alias zu Ihrer tsconfig.json hinzu:
{
"compilerOptions": {
"paths": {
"@emdash-cms/types": ["./.emdash/types.ts"]
}
}
}
Generieren Sie Typen mit dem folgenden Befehl:
npx emdash types