Un tema EmDash è un progetto Astro che un altro sviluppatore può scaffoldare con create-astro. Costruisci e testa il progetto come sito completo, poi includi un seed che crea il modello di contenuto atteso da route e componenti.
Partire da un modello attuale
Scegli il modello esistente più vicino al sito previsto e al target di deployment. Le varianti Node e Cloudflare tengono insieme database, storage, adapter e configurazione middleware.
Il modello blog è una base utile per un sito di contenuti:
npm create astro@latest -- --template @emdash-cms/template-blog
Usa @emdash-cms/template-blog-cloudflare quando il tema risultante punta a Cloudflare Workers, D1 e R2.
Mantenere la struttura del modello
Il modello blog attuale usa questi percorsi rilevanti:
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/
I modelli starter, portfolio e marketing usano route diverse. Copia i file reali dalla base scelta invece di assumere che ogni tema abbia una route catch-all per le pagine.
Puntare al seed
I modelli attuali dichiarano il percorso del seed in package.json:
{
"name": "@example/emdash-theme-publication",
"private": true,
"type": "module",
"emdash": {
"seed": "seed/seed.json"
}
}
EmDash scopre anche .emdash/seed.json e il fallback convenzionale seed/seed.json. Usa il campo del pacchetto quando distribuisci un modello così il file previsto è esplicito.
Definire il modello di contenuto
Se parti dal modello blog, modifica il suo seed/seed.json esistente invece di sostituirlo con un modello non correlato. Il seed ridotto seguente mantiene le collection e i dati strutturali usati negli esempi di questa guida:
{
"$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": []
}
}
]
}
}
L’id di una voce di contenuto è un identificatore locale al seed usato dalle reference. Non è obbligato a diventare l’ID database di una voce routabile. Il suo slug diventa l’identificatore di route esposto come entry.id dall’API di query.
Leggi File seed prima di aggiungere tassonomie, byline, reference di menu, media, redirect, aree widget, section, localizzazione o comportamento in caso di conflitto.
Costruire route renderizzate lato server
I modelli EmDash attuali usano output: "server". Le route interrogano contenuto live a ogni richiesta. Non aggiungere getStaticPaths() alle route di contenuto di un tema a meno che il modello non usi EmDash deliberatamente solo come sorgente al build.
L’archivio seguente ordina nel database usando il campo memorizzato 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 è un oggetto campo-verso-direzione. Usa orderBy: { published_at: "desc" }, non sort, sortBy o un callback JavaScript.
La route seguente risolve e renderizza un post:
---
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>
Usa post.id negli URL di route e post.data.id dove un helper richiede l’ID contenuto memorizzato.
Interrogare la navigazione gestita dal sito
I valori gestiti dal CMS devono provenire dalle API corrispondenti. I modelli attuali usano getSiteSettings(), getMenu() e <WidgetArea /> nei layout:
---
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>
Il copy di design statico può restare nei file Astro. I valori che gli amministratori devono poter modificare vanno rappresentati in impostazioni, contenuto, menu o widget.
Renderizzare campi immagine
I campi immagine sono valori media, non stringhe URL. Passa il valore completo al componente Image così storage locale e provider immagini si risolvono in modo coerente:
---
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>
Il componente usa il testo alt del campo salvo override. Riserva priority a un’immagine expected above the fold; le altre restano lazy-loaded.
Offrire scelte di layout pagina
Il seed iniziale sopra include un campo select per siti dove gli editor servono più di un layout pagina. Aggiungendo la funzione a un altro seed, usa valori stabili che mappano a componenti noti:
{
"slug": "template",
"label": "Page template",
"type": "select",
"defaultValue": "default",
"validation": {
"options": ["default", "full-width", "landing"]
}
}
La route può poi scegliere da una mappa esplicita di componenti:
---
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} />
La mappa esplicita evita che un valore di campo memorizzato diventi un percorso modulo arbitrario.
Aggiungere la ricerca
Abilita search in ogni collection che deve comparire nei risultati e segna i campi rilevanti come searchable. I modelli attuali usano LiveSearch per una route di ricerca pronta:
---
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>
Su un sito Astro i18n, LiveSearch usa Astro.currentLocale. Passa locale={null} solo quando la pagina cerca intenzionalmente ogni locale.
Seed di section riutilizzabili
Le section danno agli editor punti di partenza Portable Text riutilizzabili. Aggiungile quando il design include un pattern di contenuto ripetuto, come una call to action:
{
"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" }
]
}
]
}
]
}
La procedura guidata di setup permette di omettere voci seed, byline e termini di tassonomia. Section e il resto del modello strutturale vengono comunque applicati.
Aggiungere block di pagina strutturati
Usa un campo blocks quando la pagina stessa è una composizione ordinata di sezioni tipizzate. Usa un block Portable Text personalizzato quando l’oggetto appartiene a un documento rich text e partecipa al flusso dei paragrafi.
Il modello marketing fa seed di cinque tipi di block retained e li consente nel campo content delle Pages. Il wrapper mappa ogni _type generato a un componente Astro:
---
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} />
L’unione PageContentBlock generata mantiene la mappa componenti allineata a ogni tipo di block consentito e versione retained. Ogni valore block nel seed include _type, _version e una _key stabile.
Usa un block Portable Text personalizzato per un oggetto tra paragrafi, come un diagramma incorporato in un articolo. Mappa quell’oggetto tramite l’opzione components.type del componente PortableText. Un seed può includere il valore memorizzato, ma un plugin nativo deve registrare un editor personalizzato per creare e modificare quell’oggetto Portable Text.
Usa un plugin nativo quando il block richiede un editor personalizzato riutilizzabile e componenti di rendering impacchettati. I plugin sandbox non possono includere componenti di rendering Astro nel build del sito.
Testare il modello
-
Scaffolding del modello in una directory pulita con
create-astro. -
Installa le dipendenze ed esegui i comandi di build e typecheck del sito.
-
Parti da un database vuoto e completa
/_emdash/admin/setup. -
Testa il setup con contenuto di esempio incluso ed escluso.
-
Apri ogni route con contenuto seed, contenuto appena creato, senza immagine opzionale e con collection vuota.
-
Modifica impostazioni sito, menu, assegnazioni tassonomia, Portable Text e block dall’admin. Per i block, aggiungi, riordina, duplica e rimuovi ogni tipo consentito. Conferma che le route pubbliche usino i valori modificati.
-
Applica la configurazione specifica del deployment in un ambiente usa e getta e verifica storage media, anteprime e rendering server.
Pubblicare il modello
Un repository GitHub può essere usato direttamente con la sintassi template github: di Astro:
npm create astro@latest -- --template github:example/emdash-theme-publication
Prima della pubblicazione, rimuovi database e upload locali, tieni i secret fuori dal repository, verifica i percorsi dei pacchetti da un checkout pulito e documenta quale target di deployment configura il modello.
Checklist
-
astro.config.mjsusa output server e gli adapter di deployment previsti. -
src/live.config.tsregistraemdashLoader(). -
package.json#emdash.seedpunta a un file esistente. - Ogni collection e campo interrogati è dichiarato dal seed.
- Gli esempi di query usano
orderBye l’identificatore entry corretto. - I valori di sito gestiti dal CMS non sono duplicati come costanti permanenti del modello.
- Un setup pulito funziona con e senza contenuto di esempio.
- Build e typecheck del modello passano per il target supportato.