Ein EmDash-Theme ist ein Astro-Projekt, das andere Entwickler mit create-astro als Gerüst anlegen können. Bauen und testen Sie das Projekt als vollständige Site und fügen Sie dann einen Seed bei, der das Content-Modell erstellt, das seine Routen und Komponenten erwarten.
Von einer aktuellen Vorlage ausgehen
Wählen Sie die vorhandene Vorlage, die der geplanten Site und dem Deployment-Ziel am nächsten kommt. Die Node- und Cloudflare-Varianten halten ihre Konfiguration für Datenbank, Storage, Adapter und Middleware jeweils zusammen.
Die Blog-Vorlage ist eine nützliche Basis für eine Content-Site:
npm create astro@latest -- --template @emdash-cms/template-blog
Verwenden Sie @emdash-cms/template-blog-cloudflare, wenn das fertige Theme auf Cloudflare Workers, D1 und R2 abzielt.
Template-Struktur beibehalten
Die aktuelle Blog-Vorlage verwendet 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/
Die Vorlagen Starter, Portfolio und Marketing verwenden andere Routen. Kopieren Sie tatsächliche Dateien aus 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 erkennt auch .emdash/seed.json und den üblichen Fallback seed/seed.json. Verwenden Sie das Paketfeld, wenn Sie eine Vorlage verteilen, damit die gemeinte Datei explizit ist.
Content-Modell definieren
Wenn Sie von der Blog-Vorlage ausgehen, bearbeiten Sie deren vorhandene seed/seed.json, statt sie durch ein unzusammenhängendes Modell zu ersetzen. Der folgende reduzierte Seed enthält die Collections und strukturellen Daten, die die Beispiele in dieser Anleitung verwenden:
{
"$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 des Inhaltseintrags ist ein Seed-lokaler Bezeichner, der von Referenzen verwendet wird. Sie wird bei einem routable Eintrag nicht zwangsläufig zur Datenbank-ID. Sein slug wird zum Routenbezeichner, den die Query-API als entry.id bereitstellt.
Lesen Sie Seed-Dateien, bevor Sie Taxonomies, Bylines, Menü-Referenzen, Medien, Redirects, Widget-Bereiche, Sections, Lokalisierung oder Konfliktverhalten hinzufügen.
Serverseitig gerenderte Routen erstellen
Aktuelle EmDash-Vorlagen verwenden output: "server". Routen fragen bei jeder Anfrage Live-Inhalt ab. Fügen Sie den Inhaltsrouten eines Themes kein getStaticPaths() hinzu, es sei denn, die Vorlage verwendet EmDash bewusst nur als Quelle zur Build-Zeit.
Das folgende Archiv sortiert in der Datenbank anhand des gespeicherten Feldes 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 Objekt, das Felder auf Richtungen abbildet. Verwenden Sie orderBy: { published_at: "desc" }, nicht sort, sortBy oder einen JavaScript-Callback.
Die folgende Route löst einen einzelnen 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.rewrite("/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.rewrite("/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>
Verwenden Sie post.id in Routen-URLs und post.data.id, wo ein Helfer die gespeicherte Inhalts-ID benötigt.
Site-verwaltete Navigation abfragen
Vom CMS verwaltete Werte sollten aus den passenden APIs kommen. Die aktuellen Vorlagen verwenden in ihren Layouts getSiteSettings(), getMenu() und <WidgetArea />:
---
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>
Statische Design-Texte können in Astro-Dateien bleiben. Werte, die Administratoren bearbeiten sollen, müssen in Einstellungen, Inhalt, Menüs oder Widgets abgebildet werden.
Bildfelder rendern
Bildfelder sind Medienwerte, keine URL-Strings. Übergeben Sie den vollständigen Wert an die Komponente Image, damit lokaler Storage und Bildanbieter einheitlich 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 verwendet den eigenen Alt-Text des Feldes, sofern Sie ihn nicht überschreiben. Reservieren Sie priority für ein Bild, das voraussichtlich im sichtbaren Bereich oberhalb des Falzes liegt; andere Bilder werden weiterhin lazy geladen.
Seitenlayout-Optionen anbieten
Der obige Starter-Seed enthält ein select-Feld für Sites, auf denen Redakteure mehr als ein Seitenlayout benötigen. Wenn Sie die Funktion zu einem anderen Seed hinzufügen, verwenden Sie stabile Werte, die auf bekannte Komponenten abgebildet werden:
{
"slug": "template",
"label": "Page template",
"type": "select",
"defaultValue": "default",
"validation": {
"options": ["default", "full-width", "landing"]
}
}
Die Route kann dann aus einer expliziten Komponenten-Map auswä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.rewrite("/404");
const { entry: page } = await getEmDashEntry("pages", slug);
if (!page) return Astro.rewrite("/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 den Ergebnissen erscheinen soll, und markieren Sie die relevanten Felder als searchable. Die aktuellen Vorlagen verwenden 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 verwendet LiveSearch Astro.currentLocale. Übergeben Sie locale={null} nur, wenn die Seite bewusst alle Locales durchsucht.
Wiederverwendbare Sections seeden
Sections geben Redakteuren wiederverwendbare Portable-Text-Ausgangspunkte. Fügen Sie sie hinzu, wenn das Design ein wiederkehrendes Inhaltsmuster 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 Einrichtungsassistent lässt Benutzer per Seed angelegte Einträge, Bylines und Taxonomy-Terms weglassen. Sections und der Rest des strukturellen Modells werden trotzdem angewendet.
Strukturierte Seiten-Blocks hinzufügen
Verwenden Sie ein blocks-Feld, wenn die Seite selbst eine geordnete Komposition typisierter Sections ist. Verwenden Sie einen benutzerdefinierten Portable-Text-Block, wenn das Objekt in ein Rich-Text-Dokument gehört und am Absatzfluss teilnimmt.
Die Marketing-Vorlage legt per Seed fünf beibehaltene Blocktypen an und erlaubt sie im Feld content von Pages. Ihr Wrapper bildet jeden generierten _type auf eine Astro-Komponente ab:
---
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 Union PageContentBlock hält die Komponenten-Map mit jedem erlaubten Blocktyp und jeder beibehaltenen Version abgestimmt. Jeder per Seed angelegte Blockwert enthält _type, _version und einen stabilen _key.
Verwenden Sie einen benutzerdefinierten Portable-Text-Block für ein Objekt, das zwischen Absätze gehört, etwa ein eingebettetes Diagramm in einem Artikel. Bilden Sie dieses Objekt über die Option components.type der Komponente PortableText ab. Ein Seed kann den gespeicherten Wert enthalten, aber ein natives Plugin muss einen benutzerdefinierten Editor registrieren, um dieses Portable-Text-Objekt zu erstellen und zu bearbeiten.
Verwenden Sie ein natives Plugin, wenn der Block einen wiederverwendbaren benutzerdefinierten Editor und gepackte Rendering-Komponenten benötigt. Sandboxed Plugins können keine Astro-Rendering-Komponenten in einen Site-Build mitliefern.
Die Vorlage testen
-
Legen Sie die Vorlage mit
create-astroin einem sauberen Verzeichnis als Gerüst an. -
Installieren Sie die Abhängigkeiten und führen Sie die Build- und Typecheck-Befehle der Site aus.
-
Beginnen Sie mit einer leeren Datenbank und schließen Sie
/_emdash/admin/setupab. -
Testen Sie die Einrichtung mit und ohne Beispielinhalt.
-
Öffnen Sie jede Route mit per Seed angelegtem Inhalt, neu erstelltem Inhalt, ohne optionales Bild und mit einer leeren Collection.
-
Bearbeiten Sie Site-Einstellungen, Menüs, Taxonomy-Zuordnungen, Portable Text und Blocks über die Administration. Fügen Sie bei Blocks jeden erlaubten Typ hinzu, ordnen Sie ihn neu, duplizieren Sie ihn und entfernen Sie ihn. Bestätigen Sie, dass die öffentlichen Routen die geänderten Werte verwenden.
-
Wenden Sie die Deployment-spezifische Einrichtung in einer Wegwerfumgebung an und prüfen Sie Medien-Storage, Vorschauen und serverseitiges Rendering.
Die Vorlage veröffentlichen
Ein GitHub-Repository kann direkt mit der github:-Vorlagensyntax von Astro verwendet 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 heraus, prüfen Sie die Paketpfade in einem sauberen Checkout und dokumentieren Sie, welches Deployment-Ziel die Vorlage konfiguriert.
Checkliste
-
astro.config.mjsverwendet Server-Output und die vorgesehenen Deployment-Adapter. -
src/live.config.tsregistriertemdashLoader(). -
package.json#emdash.seedzeigt auf eine vorhandene Datei. - Jede abgefragte Collection und jedes abgefragte Feld wird vom Seed deklariert.
- Query-Beispiele verwenden
orderByund den richtigen Eintragsbezeichner. - Vom CMS verwaltete Site-Werte sind nicht als dauerhafte Template-Konstanten dupliziert.
- Eine saubere Einrichtung funktioniert mit und ohne Beispielinhalt.
- Build und Typecheck der Vorlage laufen für das unterstützte Ziel durch.