Un tema EmDash è un sito Astro completo — pagine, layout, componenti, stili — che include anche un file seed per inizializzare il modello di contenuto. Creane uno per condividere il tuo design con altri, o per standardizzare la creazione di siti per la tua agenzia.
Concetti Chiave
- Un tema è un progetto Astro funzionante. Non c’è un’API per i temi né uno strato di astrazione. Un tema è un sito fornito come template. Il file seed dice a EmDash quali collezioni, campi, menu, reindirizzamenti e tassonomie creare al primo avvio.
- Il file seed dichiara il modello di contenuto. Elenca esattamente quali campi necessita ogni collezione. Costruisci sulle collezioni standard posts e pages e aggiungi campi e tassonomie secondo le necessità del design, piuttosto che inventare tipi di contenuto completamente nuovi.
- Le pagine di contenuto del tema devono essere renderizzate lato server. In un tema, il contenuto cambia in runtime attraverso l’UI di amministrazione, quindi le pagine che visualizzano contenuto EmDash non devono essere pre-renderizzate. Non usare
getStaticPaths()nelle route di contenuto del tema. (Le build di siti statici che usano EmDash come sorgente dati al momento della build possono usaregetStaticPaths, ma i temi sono sempre SSR.) - Nessun contenuto hard-coded. Il titolo del sito, il motto, la navigazione e altri contenuti dinamici provengono dal CMS tramite chiamate API — non da stringhe di template.
Struttura del Progetto
Un tema utilizza la seguente struttura:
my-emdash-theme/
├── package.json # Metadati del tema
├── astro.config.mjs # Configurazione Astro + EmDash
├── src/
│ ├── live.config.ts # Configurazione Live Collections
│ ├── pages/
│ │ ├── index.astro # Homepage
│ │ ├── [...slug].astro # Pagine (catch-all)
│ │ ├── posts/
│ │ │ ├── index.astro # Archivio post
│ │ │ └── [slug].astro # Post singolo
│ │ ├── categories/
│ │ │ └── [slug].astro # Archivio categorie
│ │ ├── tags/
│ │ │ └── [slug].astro # Archivio tag
│ │ ├── search.astro # Pagina di ricerca
│ │ └── 404.astro # Non trovato
│ ├── layouts/
│ │ └── Base.astro # Layout base
│ └── components/ # I tuoi componenti
├── .emdash/
│ ├── seed.json # Schema e contenuto di esempio
│ └── uploads/ # File multimediali locali opzionali
└── public/ # Asset statici
Le pagine si trovano nella root come route catch-all ([...slug].astro), quindi una pagina con slug about viene renderizzata a /about. Post, categorie e tag ottengono le loro directory. La directory .emdash/ contiene il file seed e tutti i file multimediali locali usati nel contenuto di esempio.
Configurazione di package.json
Aggiungi il campo emdash al tuo package.json:
{
"name": "@your-org/emdash-theme-blog",
"version": "1.0.0",
"description": "A minimal blog theme for EmDash",
"keywords": ["astro-template", "emdash", "blog"],
"emdash": {
"label": "Minimal Blog",
"description": "A clean, minimal blog with posts, pages, and categories",
"seed": ".emdash/seed.json",
"preview": "https://your-theme-demo.pages.dev"
}
}
| Campo | Descrizione |
|---|---|
emdash.label | Nome visualizzato nei selettori di temi |
emdash.description | Breve descrizione del tema |
emdash.seed | Percorso al file seed |
emdash.preview | URL di una demo live (opzionale) |
Il Modello di Contenuto Predefinito
La maggior parte dei temi necessita di due tipi di collezioni: posts e pages. I post sono voci con timestamp con estratti e immagini in evidenza che appaiono nei feed e negli archivi. Le pagine sono contenuti autonomi a URL di primo livello.
Questo è il punto di partenza consigliato. Aggiungi più collezioni, tassonomie o campi secondo le necessità del tuo tema, ma inizia qui.
File Seed
Il file seed dice a EmDash cosa creare al primo avvio. Crea .emdash/seed.json:
{
"$schema": "https://emdashcms.com/seed.schema.json",
"version": "1",
"meta": {
"name": "Minimal Blog",
"description": "A clean blog with posts and pages",
"author": "Your Name"
},
"settings": {
"title": "My Blog",
"tagline": "Thoughts and ideas",
"postsPerPage": 10
},
"collections": [
{
"slug": "posts",
"label": "Posts",
"labelSingular": "Post",
"supports": ["drafts", "revisions"],
"fields": [
{ "slug": "title", "label": "Title", "type": "string", "required": true },
{ "slug": "content", "label": "Content", "type": "portableText" },
{ "slug": "excerpt", "label": "Excerpt", "type": "text" },
{ "slug": "featured_image", "label": "Featured Image", "type": "image" }
]
},
{
"slug": "pages",
"label": "Pages",
"labelSingular": "Page",
"supports": ["drafts", "revisions"],
"fields": [
{ "slug": "title", "label": "Title", "type": "string", "required": true },
{ "slug": "content", "label": "Content", "type": "portableText" }
]
}
],
"taxonomies": [
{
"name": "category",
"label": "Categories",
"labelSingular": "Category",
"hierarchical": true,
"collections": ["posts"],
"terms": [
{ "slug": "news", "label": "News" },
{ "slug": "tutorials", "label": "Tutorials" }
]
}
],
"menus": [
{
"name": "primary",
"label": "Primary Navigation",
"items": [
{ "type": "custom", "label": "Home", "url": "/" },
{ "type": "custom", "label": "Blog", "url": "/posts" }
]
}
],
"redirects": [
{ "source": "/category/news", "destination": "/categories/news" },
{ "source": "/old-about", "destination": "/about" }
]
}
I post ottengono excerpt e featured_image perché appaiono in elenchi e feed. Le pagine non ne hanno bisogno — sono contenuti autonomi. Aggiungi campi a entrambe le collezioni secondo le necessità del tuo tema.
Vedi Formato del File Seed per la specifica completa, incluse sezioni, aree widget e riferimenti multimediali.
Costruzione delle Pagine
Tutte le pagine che visualizzano contenuto EmDash sono renderizzate lato server. Usa Astro.params per ottenere lo slug dall’URL e interrogare il contenuto al momento della richiesta.
Homepage
---
import { getEmDashCollection, getSiteSettings } from "emdash";
import Base from "../layouts/Base.astro";
const settings = await getSiteSettings();
const { entries: posts } = await getEmDashCollection("posts", {
where: { status: "published" },
orderBy: { publishedAt: "desc" },
limit: settings.postsPerPage ?? 10,
});
---
<Base title="Home">
<h1>Latest Posts</h1>
{posts.map((post) => (
<article>
<h2><a href={`/posts/${post.slug}`}>{post.data.title}</a></h2>
<p>{post.data.excerpt}</p>
</article>
))}
</Base>
Post Singolo
---
import { getEmDashEntry, getEntryTerms } from "emdash";
import { PortableText } from "emdash/ui";
import Base from "../../layouts/Base.astro";
const { slug } = Astro.params;
const { entry: post } = await getEmDashEntry("posts", slug!);
if (!post) {
return Astro.redirect("/404");
}
const categories = await getEntryTerms("posts", post.id, "categories");
---
<Base title={post.data.title}>
<article>
<h1>{post.data.title}</h1>
<PortableText value={post.data.content} />
<div class="post-meta">
{categories.map((cat) => (
<a href={`/categories/${cat.slug}`}>{cat.label}</a>
))}
</div>
</article>
</Base>
Pages
Le pagine usano una route catch-all nella root in modo che i loro slug siano mappati direttamente su URL di primo livello — una pagina con slug about viene renderizzata a /about:
---
import { getEmDashEntry } from "emdash";
import { PortableText } from "emdash/ui";
import Base from "../layouts/Base.astro";
const { slug } = Astro.params;
const { entry: page } = await getEmDashEntry("pages", slug!);
if (!page) {
return Astro.redirect("/404");
}
---
<Base title={page.data.title}>
<article>
<h1>{page.data.title}</h1>
<PortableText value={page.data.content} />
</article>
</Base>
Poiché questa è una route catch-all, corrisponde solo a URL che non hanno una route più specifica. /posts/hello-world raggiunge ancora posts/[slug].astro, non questo file.
Archivio Categorie
---
import { getTerm, getEntriesByTerm } from "emdash";
import Base from "../../layouts/Base.astro";
const { slug } = Astro.params;
const category = await getTerm("categories", slug!);
const posts = await getEntriesByTerm("posts", "categories", slug!);
if (!category) {
return Astro.redirect("/404");
}
---
<Base title={category.label}>
<h1>{category.label}</h1>
{posts.map((post) => (
<article>
<h2><a href={`/posts/${post.slug}`}>{post.data.title}</a></h2>
</article>
))}
</Base>
Uso delle Immagini
I campi immagine sono oggetti con proprietà src e alt, non stringhe. Usa il componente Image da emdash/ui per il rendering ottimizzato delle immagini:
---
import { Image } from "emdash/ui";
const { post } = Astro.props;
---
<article>
{post.data.featured_image?.src && (
<Image
image={post.data.featured_image}
alt={post.data.featured_image.alt || post.data.title}
width={800}
height={450}
/>
)}
<h2><a href={`/posts/${post.slug}`}>{post.data.title}</a></h2>
<p>{post.data.excerpt}</p>
</article>
Uso dei Menu
Interroga i menu definiti dall’amministratore nei tuoi layout. Non hard-codare mai i link di navigazione:
---
import { getMenu, getSiteSettings } from "emdash";
const settings = await getSiteSettings();
const primaryMenu = await getMenu("primary");
---
<html>
<head>
<title>{Astro.props.title} | {settings.title}</title>
</head>
<body>
<header>
{settings.logo ? (
<img src={settings.logo.url} alt={settings.title} />
) : (
<span>{settings.title}</span>
)}
<nav>
{primaryMenu?.items.map((item) => (
<a href={item.url}>{item.label}</a>
))}
</nav>
</header>
<main>
<slot />
</main>
</body>
</html>
Template di Pagina
I temi spesso necessitano di più layout di pagina — un layout predefinito, un layout a larghezza piena, un layout di landing page. In EmDash, aggiungi un campo select template alla collezione pages e mappalo su componenti di layout nella tua route catch-all.
Aggiungi il campo alla collezione pages nel file seed:
{
"slug": "template",
"label": "Page Template",
"type": "string",
"widget": "select",
"options": {
"choices": [
{ "value": "default", "label": "Default" },
{ "value": "full-width", "label": "Full Width" },
{ "value": "landing", "label": "Landing Page" }
]
},
"defaultValue": "default"
}
Quindi mappa il valore su componenti di layout nella route catch-all:
---
import { getEmDashEntry } from "emdash";
import PageDefault from "../layouts/PageDefault.astro";
import PageFullWidth from "../layouts/PageFullWidth.astro";
import PageLanding from "../layouts/PageLanding.astro";
const { slug } = Astro.params;
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={page} />
Gli editori scelgono il template da un menu a tendina nell’UI di amministrazione quando modificano una pagina.
Aggiunta di Sezioni
Le sezioni sono blocchi di contenuto riutilizzabili che gli editori possono inserire in qualsiasi campo Portable Text usando il comando slash /section. Se il tuo tema ha pattern di contenuto comuni (banner hero, CTA, griglie di funzionalità), definiscili come sezioni nel file seed:
{
"sections": [
{
"slug": "hero-centered",
"title": "Centered Hero",
"description": "Full-width hero with centered heading and CTA",
"keywords": ["hero", "banner", "header", "landing"],
"content": [
{
"_type": "block",
"style": "h1",
"children": [{ "_type": "span", "text": "Welcome to Our Site" }]
},
{
"_type": "block",
"children": [
{ "_type": "span", "text": "Your compelling tagline goes here." }
]
}
]
},
{
"slug": "newsletter-cta",
"title": "Newsletter Signup",
"keywords": ["newsletter", "subscribe", "email"],
"content": [
{
"_type": "block",
"style": "h3",
"children": [{ "_type": "span", "text": "Subscribe to our newsletter" }]
},
{
"_type": "block",
"children": [
{
"_type": "span",
"text": "Get the latest updates delivered to your inbox."
}
]
}
]
}
]
}
Le sezioni create dal file seed sono contrassegnate con source: "theme". Gli editori possono anche creare le proprie sezioni (contrassegnate source: "user"), ma le sezioni fornite dal tema non possono essere eliminate dall’UI di amministrazione.
Aggiunta di Contenuto di Esempio
Includi contenuto di esempio nel file seed per dimostrare il design del tuo tema:
{
"content": {
"posts": [
{
"id": "hello-world",
"slug": "hello-world",
"status": "published",
"data": {
"title": "Hello World",
"content": [
{
"_type": "block",
"style": "normal",
"children": [{ "_type": "span", "text": "Welcome to your new blog!" }]
}
],
"excerpt": "Your first post on EmDash."
},
"taxonomies": {
"category": ["news"]
}
}
]
}
}
Inclusione di Media
Riferisci immagini nel contenuto di esempio usando la sintassi $media. Un’immagine remota è riferita per URL:
{
"data": {
"featured_image": {
"$media": {
"url": "https://images.unsplash.com/photo-xxx",
"alt": "A descriptive alt text",
"filename": "hero.jpg"
}
}
}
}
Per immagini locali, posiziona i file in .emdash/uploads/ e riferiscili per nome file:
{
"data": {
"featured_image": {
"$media": {
"file": "hero.jpg",
"alt": "A descriptive alt text"
}
}
}
}
Durante il seeding, i file multimediali vengono scaricati (o letti localmente) e caricati nello storage.
Ricerca
Se il tuo tema include una pagina di ricerca, usa il componente LiveSearch per risultati istantanei:
---
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>
LiveSearch fornisce ricerca istantanea con debounce con corrispondenza prefissi, stemming Porter e snippet di risultati evidenziati. La ricerca deve essere abilitata per collezione nell’UI di amministrazione (Content Types > Edit > Features > Search).
Test del Tuo Tema
-
Crea un progetto di test dal tuo tema:
npm create astro@latest -- --template ./path/to/my-theme -
Installa le dipendenze e avvia il dev server:
cd test-site npm install npm run dev -
Completa il Setup Wizard a
http://localhost:4321/_emdash/admin -
Verifica che collezioni, menu, redirect e contenuto siano stati creati correttamente
-
Testa che tutti i template di pagina si renderizzino correttamente
-
Crea nuovo contenuto tramite l’UI di amministrazione per verificare che tutti i campi funzionino
Pubblicazione del Tuo Tema
Pubblica su npm per la distribuzione:
npm publish --access public
Gli utenti possono quindi installare il tuo tema:
npm create astro@latest -- --template @your-org/emdash-theme-blog
Un tema ospitato su GitHub viene installato con il prefisso template github::
npm create astro@latest -- --template github:your-org/emdash-theme-blog
Blocchi Portable Text Personalizzati
I temi possono definire tipi di blocchi Portable Text personalizzati per contenuto specializzato. Questo è utile per pagine marketing, landing page o qualsiasi contenuto che necessita componenti strutturati oltre al rich text standard.
Definizione di Blocchi Personalizzati nel Contenuto Seed
Usa un _type con namespace nel contenuto Portable Text del tuo file seed:
{
"content": {
"pages": [
{
"id": "home",
"slug": "home",
"status": "published",
"data": {
"title": "Home",
"content": [
{
"_type": "marketing.hero",
"headline": "Build something amazing",
"subheadline": "The all-in-one platform for modern teams.",
"primaryCta": { "label": "Get Started", "url": "/signup" }
},
{
"_type": "marketing.features",
"_key": "features",
"headline": "Everything you need",
"features": [
{
"icon": "zap",
"title": "Lightning fast",
"description": "Built for speed."
}
]
}
]
}
}
]
}
}
Creazione di Componenti Blocco
Crea componenti Astro per ogni tipo di blocco personalizzato:
---
interface Props {
value: {
headline: string;
subheadline?: string;
primaryCta?: { label: string; url: string };
};
}
const { value } = Astro.props;
---
<section class="hero">
<h1>{value.headline}</h1>
{value.subheadline && <p>{value.subheadline}</p>}
{value.primaryCta && (
<a href={value.primaryCta.url} class="btn">
{value.primaryCta.label}
</a>
)}
</section>
Rendering di Blocchi Personalizzati
Passa i tuoi componenti blocco personalizzati al componente PortableText:
---
import { PortableText } from "emdash/ui";
import Hero from "./blocks/Hero.astro";
import Features from "./blocks/Features.astro";
interface Props {
value: unknown[];
}
const { value } = Astro.props;
const marketingTypes = {
"marketing.hero": Hero,
"marketing.features": Features,
};
---
<PortableText value={value} components={{ types: marketingTypes }} />
Renderizza il componente wrapper in una pagina:
---
import { getEmDashEntry } from "emdash";
import MarketingBlocks from "../components/MarketingBlocks.astro";
const { entry: page } = await getEmDashEntry("pages", "home");
---
<MarketingBlocks value={page.data.content} />
ID Ancora per la Navigazione
Aggiungi _key ai blocchi che dovrebbero essere collegabili:
{
"_type": "marketing.features",
"_key": "features",
"headline": "Features"
}
Usa il valore _key come ancora nel componente blocco:
<section id={value._key}>
<!-- content -->
</section>
Questo abilita link di navigazione come /#features.
Checklist del Tema
Prima di pubblicare, verifica che il tuo tema includa:
-
package.jsoncon campoemdash(label, descrizione, percorso seed) -
.emdash/seed.jsoncon schema valido - Tutte le collezioni riferite nelle pagine esistono nel seed
- I menu usati nei layout sono definiti nel seed
- Il contenuto di esempio dimostra il design del tema
-
astro.config.mjscon configurazione database e storage -
src/live.config.tscon loader EmDash - Nessun
getStaticPaths()su pagine di contenuto - Nessun titolo sito, motto o navigazione hard-coded
- I campi immagine sono accessibili come oggetti (
image.src), non stringhe - README con istruzioni di setup
- Componenti blocco personalizzati per qualsiasi tipo Portable Text non standard
Prossimi Passi
- Formato del File Seed — Riferimento completo per i file seed
- Panoramica Temi — Come funzionano i temi in EmDash
- Portare Temi WordPress — Convertire temi WordPress esistenti