Ein EmDash-Theme ist ein Astro-Projekt, das ein anderer Entwickler mit create-astro scaffolden kann. Bauen und testen Sie das Projekt als vollständige Site, und fügen Sie einen Seed hinzu, der das von den Routen und Komponenten erwartete Content-Modell anlegt.
Von einer aktuellen Vorlage ausgehen
Wählen Sie die bestehende Vorlage, die der geplanten Site und dem Deployment-Ziel am nächsten kommt. Die Node- und Cloudflare-Varianten halten Datenbank, Storage, Adapter und Middleware-Konfiguration zusammen.
Die Blog-Vorlage eignet sich gut als Basis für eine Content-Site:
npm create astro@latest -- --template @emdash-cms/template-blog
Verwenden Sie @emdash-cms/template-blog-cloudflare, wenn das resultierende Theme Cloudflare Workers, D1 und R2 anvisiert.
Template-Struktur beibehalten
Die aktuelle Blog-Vorlage nutzt diese relevanten Pfade:
astro.config.mjs
emdash-env.d.ts
package.json
seed/
└── seed.json
src/
├── components/
│ └── PostCard.astro
├── layouts/
│ └── Base.astro
├── live.config.ts
├── pages/
│ ├── index.astro
│ ├── category/[slug].astro
│ ├── pages/[slug].astro
│ ├── posts/index.astro
│ ├── posts/[slug].astro
│ ├── search.astro
│ └── tag/[slug].astro
└── styles/
Starter-, Portfolio- und Marketing-Vorlagen nutzen andere Routen. Kopieren Sie die tatsächlichen Dateien von der gewählten Basis, statt anzunehmen, dass jedes Theme eine Catch-all-Seitenroute hat.
Auf den Seed verweisen
Aktuelle Vorlagen deklarieren ihren Seed-Pfad in package.json:
{
"name": "@example/emdash-theme-publication",
"private": true,
"type": "module",
"emdash": {
"seed": "seed/seed.json"
}
}
EmDash findet auch .emdash/seed.json und den konventionellen Fallback seed/seed.json. Nutzen Sie das Paketfeld bei der Verteilung einer Vorlage, damit die beabsichtigte Datei explizit ist.
Content-Modell definieren
Wenn Sie von der Blog-Vorlage ausgehen, bearbeiten Sie deren vorhandene seed/seed.json, statt sie durch ein unabhängiges Modell zu ersetzen. Der folgende reduzierte Seed behält die Collections und strukturellen Daten bei, die in den Beispielen dieses Leitfadens verwendet werden:
{
"$schema": "https://emdashcms.com/seed.schema.json",
"version": "1",
"meta": {
"name": "Publication",
"description": "A publication with posts"
},
"settings": {
"title": "Publication",
"tagline": "Latest articles"
},
"collections": [
{
"slug": "posts",
"label": "Posts",
"labelSingular": "Post",
"supports": ["drafts", "revisions", "search", "seo"],
"fields": [
{
"slug": "title",
"label": "Title",
"type": "string",
"required": true,
"searchable": true
},
{
"slug": "excerpt",
"label": "Excerpt",
"type": "text"
},
{
"slug": "featured_image",
"label": "Featured image",
"type": "image"
},
{
"slug": "content",
"label": "Content",
"type": "portableText",
"searchable": true
}
]
},
{
"slug": "pages",
"label": "Pages",
"labelSingular": "Page",
"supports": ["drafts", "revisions", "search"],
"fields": [
{
"slug": "title",
"label": "Title",
"type": "string",
"required": true,
"searchable": true
},
{
"slug": "content",
"label": "Content",
"type": "portableText",
"searchable": true
},
{
"slug": "template",
"label": "Page template",
"type": "select",
"defaultValue": "default",
"validation": {
"options": ["default", "full-width", "landing"]
}
}
]
}
],
"menus": [
{
"name": "primary",
"label": "Primary navigation",
"items": [
{ "type": "custom", "label": "Home", "url": "/" },
{ "type": "custom", "label": "Posts", "url": "/posts" }
]
}
],
"widgetAreas": [
{
"name": "sidebar",
"label": "Sidebar",
"widgets": []
}
],
"content": {
"posts": [
{
"id": "post-welcome",
"slug": "welcome",
"status": "published",
"data": {
"title": "Welcome",
"excerpt": "The first article",
"content": []
}
}
]
}
}
Die id eines Content-Eintrags ist ein seed-lokaler Bezeichner für Referenzen. Sie muss nicht zur Datenbank-ID eines routbaren Eintrags werden. Der slug wird zum Routen-Identifikator, den die Query-API als entry.id bereitstellt.
Lesen Sie Seed-Dateien, bevor Sie Taxonomien, Bylines, Menü-Referenzen, Medien, Redirects, Widget-Bereiche, Sections, Lokalisierung oder Konfliktverhalten hinzufügen.
Serverseitig gerenderte Routen erstellen
Aktuelle EmDash-Vorlagen nutzen output: "server". Routen fragen Live-Content pro Request ab. Fügen Sie Content-Routen eines Themes kein getStaticPaths() hinzu, es sei denn, die Vorlage nutzt EmDash absichtlich nur als Build-time-Quelle.
Das folgende Archiv sortiert in der Datenbank über das gespeicherte Feld published_at:
---
import { getEmDashCollection } from "emdash";
import Base from "../../layouts/Base.astro";
const { entries: posts, error, cacheHint } = await getEmDashCollection("posts", {
orderBy: { published_at: "desc" },
});
if (error) return new Response("Could not load posts", { status: 500 });
if (Astro.cache?.enabled) Astro.cache.set(cacheHint);
---
<Base title="Posts">
{posts.map((post) => (
<article>
<h2><a href={`/posts/${post.id}`}>{post.data.title}</a></h2>
{post.data.excerpt && <p>{post.data.excerpt}</p>}
</article>
))}
</Base>
orderBy ist ein Feld-zu-Richtung-Objekt. Verwenden Sie orderBy: { published_at: "desc" }, nicht sort, sortBy oder einen JavaScript-Callback.
Die folgende Route löst einen Beitrag auf und rendert ihn:
---
import { decodeSlug, getEmDashEntry } from "emdash";
import { PortableText } from "emdash/ui";
import Base from "../../layouts/Base.astro";
const slug = decodeSlug(Astro.params.slug);
if (!slug) return Astro.redirect("/404");
const { entry: post, error, cacheHint } = await getEmDashEntry("posts", slug);
if (error) return new Response("Could not load the post", { status: 500 });
if (!post) return Astro.redirect("/404");
if (Astro.cache?.enabled) Astro.cache.set(cacheHint);
---
<Base title={post.data.title} content={{ collection: "posts", id: post.data.id, slug }}>
<article>
<h1 {...post.edit.title}>{post.data.title}</h1>
<PortableText value={post.data.content} />
</article>
</Base>
Nutzen Sie post.id in Routen-URLs und post.data.id, wo ein Helper die gespeicherte Content-ID benötigt.
Site-verwaltete Navigation abfragen
CMS-verwaltete Werte sollten von den passenden APIs kommen. Die aktuellen Vorlagen nutzen getSiteSettings(), getMenu() und <WidgetArea /> in ihren Layouts:
---
import { getMenu, getSiteSettings } from "emdash";
import { WidgetArea } from "emdash/ui";
const [settings, primary] = await Promise.all([
getSiteSettings(),
getMenu("primary"),
]);
---
<header>
<a href="/">{settings.title}</a>
<nav>
{primary?.items.map((item) => <a href={item.url}>{item.label}</a>)}
</nav>
</header>
<main><slot /></main>
<aside><WidgetArea name="sidebar" /></aside>
Statischer Design-Text kann in Astro-Dateien bleiben. Werte, die Administratoren bearbeiten sollen, müssen in Settings, Content, Menüs oder Widgets abgebildet sein.
Bildfelder rendern
Image-Felder sind Medienwerte, keine URL-Zeichenketten. Übergeben Sie den vollständigen Wert an die Image-Komponente, damit lokaler Storage und Image-Provider konsistent aufgelöst werden:
---
import { Image } from "emdash/ui";
const { post } = Astro.props;
---
<article>
{post.data.featured_image && (
<Image
image={post.data.featured_image}
alt={post.data.title}
width={800}
height={450}
/>
)}
<h2><a href={`/posts/${post.id}`}>{post.data.title}</a></h2>
</article>
Die Komponente nutzt den Alt-Text des Felds, sofern Sie ihn nicht überschreiben. Reservieren Sie priority für ein Bild, das above the fold erwartet wird; andere Bilder bleiben lazy-loaded.
Seitenlayout-Optionen anbieten
Der Starter-Seed oben enthält ein select-Feld für Sites, bei denen Redakteure mehr als ein Seitenlayout brauchen. Wenn Sie die Funktion in einem anderen Seed ergänzen, verwenden Sie stabile Werte, die bekannten Komponenten zugeordnet sind:
{
"slug": "template",
"label": "Page template",
"type": "select",
"defaultValue": "default",
"validation": {
"options": ["default", "full-width", "landing"]
}
}
Die Route kann dann aus einer expliziten Komponenten-Map wählen:
---
import { decodeSlug, getEmDashEntry } from "emdash";
import PageDefault from "../../layouts/PageDefault.astro";
import PageFullWidth from "../../layouts/PageFullWidth.astro";
import PageLanding from "../../layouts/PageLanding.astro";
const slug = decodeSlug(Astro.params.slug);
if (!slug) return Astro.redirect("/404");
const { entry: page } = await getEmDashEntry("pages", slug);
if (!page) return Astro.redirect("/404");
const layouts = {
default: PageDefault,
"full-width": PageFullWidth,
landing: PageLanding,
};
const Layout = layouts[page.data.template as keyof typeof layouts] ?? PageDefault;
---
<Layout {page} />
Die explizite Map verhindert, dass ein gespeicherter Feldwert zu einem beliebigen Modulpfad wird.
Suche hinzufügen
Aktivieren Sie search in jeder Collection, die in Ergebnissen erscheinen soll, und markieren Sie relevante Felder als searchable. Die aktuellen Vorlagen nutzen LiveSearch für eine fertige Suchroute:
---
import LiveSearch from "emdash/ui/search";
import Base from "../layouts/Base.astro";
---
<Base title="Search">
<h1>Search</h1>
<LiveSearch placeholder="Search posts and pages" collections={["posts", "pages"]} />
</Base>
Auf einer Astro-i18n-Site nutzt LiveSearch Astro.currentLocale. Übergeben Sie locale={null} nur, wenn die Seite absichtlich jede Locale durchsucht.
Wiederverwendbare Sections seeden
Sections geben Redakteuren wiederverwendbare Portable-Text-Startpunkte. Fügen Sie sie hinzu, wenn das Design ein wiederholtes Content-Muster wie einen Call-to-Action enthält:
{
"version": "1",
"sections": [
{
"slug": "newsletter-signup",
"title": "Newsletter signup",
"description": "Heading and copy for the newsletter form",
"keywords": ["newsletter", "email"],
"content": [
{
"_type": "block",
"_key": "newsletter-heading",
"style": "h2",
"children": [
{ "_type": "span", "_key": "newsletter-heading-text", "text": "Get new articles by email" }
]
}
]
}
]
}
Der Setup-Assistent lässt Nutzer geseedete Einträge, Bylines und Taxonomie-Terms weglassen. Sections und der Rest des strukturellen Modells werden weiterhin angewendet.
Strukturierte Seiten-Blocks hinzufügen
Nutzen Sie ein blocks-Feld, wenn die Seite selbst eine geordnete Komposition typisierter Abschnitte ist. Nutzen Sie einen benutzerdefinierten Portable-Text-Block, wenn das Objekt in einem Rich-Text-Dokument gehört und am Absatzfluss teilnimmt.
Die Marketing-Vorlage seeded fünf beibehaltene Block-Typen und erlaubt sie im content-Feld der Pages. Ihr Wrapper mappt jeden generierten _type auf eine Astro-Komponente:
---
import { Blocks, defineBlockComponents } from "emdash/ui";
import type { PageContentBlock } from "../../emdash-env";
import FAQ from "./blocks/FAQ.astro";
import Features from "./blocks/Features.astro";
import Hero from "./blocks/Hero.astro";
import Pricing from "./blocks/Pricing.astro";
import Testimonials from "./blocks/Testimonials.astro";
const components = defineBlockComponents<PageContentBlock>({
marketing_hero: Hero,
marketing_features: Features,
marketing_testimonials: Testimonials,
marketing_pricing: Pricing,
marketing_faq: FAQ,
});
---
<Blocks value={Astro.props.value} components={components} />
Die generierte PageContentBlock-Union hält die Komponenten-Map mit jedem erlaubten Block-Typ und beibehaltenen Version im Einklang. Jeder geseedete Block-Wert enthält _type, _version und einen stabilen _key.
Nutzen Sie einen benutzerdefinierten Portable-Text-Block für ein Objekt zwischen Absätzen, etwa ein eingebettetes Diagramm in einem Artikel. Mappen Sie dieses Objekt über die Option components.type der PortableText-Komponente. Ein Seed kann den gespeicherten Wert enthalten, aber ein natives Plugin muss einen benutzerdefinierten Editor zum Erstellen und Bearbeiten dieses Portable-Text-Objekts registrieren.
Nutzen Sie ein natives Plugin, wenn der Block einen wiederverwendbaren benutzerdefinierten Editor und gebündelte Rendering-Komponenten braucht. Sandbox-Plugins können keine Astro-Rendering-Komponenten in einen Site-Build liefern.
Die Vorlage testen
-
Scaffolden Sie die Vorlage mit
create-astroin ein sauberes Verzeichnis. -
Installieren Sie Abhängigkeiten und führen Sie Build- und Typecheck-Befehle der Site aus.
-
Starten Sie mit einer leeren Datenbank und schließen Sie
/_emdash/admin/setupab. -
Testen Sie das Setup mit und ohne Beispiel-Content.
-
Öffnen Sie jede Route mit geseedetem Content, neu erstelltem Content, ohne optionales Bild und mit leerer Collection.
-
Bearbeiten Sie Site-Settings, Menüs, Taxonomie-Zuweisungen, Portable Text und Blocks im Admin. Fügen Sie bei Blocks jeden erlaubten Typ hinzu, sortieren, duplizieren und entfernen Sie ihn. Prüfen Sie, dass die öffentlichen Routen die geänderten Werte nutzen.
-
Wenden Sie die deployment-spezifische Einrichtung in einer Wegwerf-Umgebung an und prüfen Sie Media-Storage, Previews und Server-Rendering.
Die Vorlage veröffentlichen
Ein GitHub-Repository kann direkt mit Astros github:-Template-Syntax genutzt werden:
npm create astro@latest -- --template github:example/emdash-theme-publication
Entfernen Sie vor der Veröffentlichung lokale Datenbanken und Uploads, halten Sie Secrets aus dem Repository, prüfen Sie Paketpfade von einem sauberen Checkout und dokumentieren Sie, welches Deployment-Ziel die Vorlage konfiguriert.
Checkliste
-
astro.config.mjsnutzt Server-Output und die beabsichtigten Deployment-Adapter. -
src/live.config.tsregistriertemdashLoader(). -
package.json#emdash.seedzeigt auf eine vorhandene Datei. - Jede abgefragte Collection und jedes Feld ist im Seed deklariert.
- Query-Beispiele nutzen
orderByund den korrekten Entry-Identifikator. - CMS-verwaltete Site-Werte sind nicht als permanente Template-Konstanten dupliziert.
- Ein sauberes Setup funktioniert mit und ohne Beispiel-Content.
- Template-Build und Typecheck bestehen für das unterstützte Ziel.