Un tema EmDash è un sito Astro completo — pagine, layout, componenti, stili — che include anche un file seed per avviare il modello di contenuto. Creane uno per condividere il tuo design con altri o per standardizzare la creazione di siti nella tua agenzia.
Concetti chiave
- Un tema è un progetto Astro funzionante. Non esiste un’API tema né un livello di astrazione. Costruisci un sito e lo distribuisci come template. Il file seed dice semplicemente a EmDash quali collezioni, campi, menu, redirect e tassonomie creare alla prima esecuzione.
- EmDash offre più controllo sul modello di contenuto rispetto a WordPress. I temi ne approfittano — il file seed dichiara esattamente quali campi servono a ogni collezione. Parti dalle collezioni standard posts e pages e aggiungi campi e tassonomie come richiede il tuo design, anziché inventare tipi di contenuto completamente nuovi.
- Le pagine di contenuto del tema devono essere renderizzate lato server. In un tema, il contenuto cambia a runtime tramite l’UI admin, quindi le pagine che mostrano contenuti EmDash non devono essere prerenderizzate. Non usare
getStaticPaths()nelle route di contenuto del tema. (Le build statiche che usano EmDash come fonte dati al build time possono usaregetStaticPaths, ma i temi sono sempre SSR.) - Nessun contenuto codificato. Titolo del sito, tagline, navigazione e altri contenuti dinamici provengono dal CMS tramite chiamate API — non da stringhe nel template.
Struttura del progetto
Crea un tema con questa 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 articoli
│ │ │ └── [slug].astro # Articolo singolo
│ │ ├── categories/
│ │ │ └── [slug].astro # Archivio categoria
│ │ ├── tags/
│ │ │ └── [slug].astro # Archivio tag
│ │ ├── search.astro # Pagina di ricerca
│ │ └── 404.astro # Non trovato
│ ├── layouts/
│ │ └── Base.astro # Layout di base
│ └── components/ # I tuoi componenti
├── .emdash/
│ ├── seed.json # Schema e contenuti di esempio
│ └── uploads/ # File media locali opzionali
└── public/ # Asset statici
Le pagine si trovano alla root come route catch-all ([...slug].astro), quindi una pagina con slug about viene renderizzata su /about. Articoli, categorie e tag hanno le proprie directory. La directory .emdash/ contiene il file seed e qualsiasi file media locale usato nei contenuti di esempio.
Configurare 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 tema |
emdash.description | Breve descrizione del tema |
emdash.seed | Percorso del 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 collezione: posts e pages. I post sono voci con data, estratto e immagine in evidenza che compaiono in feed e 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 esigenze del tema, ma parti da qui.
File seed
Il file seed indica a EmDash cosa creare alla prima esecuzione. 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 hanno excerpt e featured_image perché compaiono in elenchi e feed. Le pagine non ne hanno bisogno — sono contenuti autonomi. Aggiungi campi a entrambe le collezioni secondo le esigenze del tema.
Vedi Formato file seed per la specifica completa, incluse sezioni, aree widget e riferimenti ai media.
Costruire le pagine
Tutte le pagine che mostrano contenuti EmDash sono renderizzate lato server. Usa Astro.params per ottenere lo slug dall’URL e interrogare i contenuti al tempo 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>
Articolo 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>
Pagine
Le pagine usano una route catch-all alla root, così i loro slug corrispondono direttamente a URL di primo livello — una pagina con slug about viene renderizzata su /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>
Essendo una route catch-all, corrisponde solo a URL che non hanno una route più specifica. /posts/hello-world colpisce comunque 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>
Usare le immagini
I campi immagine sono oggetti con proprietà src e alt, non stringhe. Usa il componente Image da emdash/ui per un rendering ottimizzato:
---
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>
Usare i menu
Interroga i menu definiti dall’admin nei tuoi layout. Non codificare 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, uno a larghezza piena, uno landing page. In EmDash, aggiungi un campo select template alla collezione pagine e mappalo ai componenti layout nella route catch-all.
Aggiungi il campo alla collezione pagine 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"
}
Poi mappa il valore ai componenti 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 editor scelgono il template da un menu a tendina nell’UI admin quando modificano una pagina.
Aggiungere sezioni
Le sezioni sono blocchi di contenuto riutilizzabili che gli editor possono inserire in qualsiasi campo Portable Text usando il comando slash /section. Se il tuo tema ha pattern di contenuto comuni (hero banner, 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 editor possono anche creare le proprie sezioni (contrassegnate source: "user"), ma le sezioni fornite dal tema non possono essere eliminate dall’UI admin.
Aggiungere contenuti di esempio
Includi contenuti di esempio nel file seed per dimostrare il design del 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"]
}
}
]
}
}
Includere media
Fai riferimento alle immagini nei contenuti di esempio usando la sintassi $media.
Per immagini remote:
{
"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 fai riferimento ad essi:
{
"data": {
"featured_image": {
"$media": {
"file": "hero.jpg",
"alt": "A descriptive alt text"
}
}
}
}
Durante il seeding, i file media vengono scaricati (o letti localmente) e caricati nello storage.
Ricerca
Se il 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, corrispondenza per prefisso, stemming Porter ed evidenziazione degli snippet nei risultati. La ricerca deve essere abilitata per ogni collezione nell’UI admin (Tipi di contenuto > Modifica > Funzionalità > Ricerca).
Testare il tema
-
Crea un progetto di test dal tuo tema:
npm create astro@latest -- --template ./path/to/my-theme -
Installa le dipendenze e avvia il server di sviluppo:
cd test-site npm install npm run dev -
Completa la procedura guidata su
http://localhost:4321/_emdash/admin -
Verifica che collezioni, menu, redirect e contenuti siano stati creati correttamente
-
Testa che tutti i template di pagina vengano renderizzati correttamente
-
Crea nuovi contenuti tramite l’admin per verificare che tutti i campi funzionino
Pubblicare il tema
Pubblica su npm per la distribuzione:
npm publish --access public
Gli utenti potranno poi installare il tema:
npm create astro@latest -- --template @your-org/emdash-theme-blog
Per temi ospitati su GitHub:
npm create astro@latest -- --template github:your-org/emdash-theme-blog
Blocchi Portable Text personalizzati
I temi possono definire tipi di blocco Portable Text personalizzati per contenuti specializzati. Questo è utile per pagine marketing, landing page o qualsiasi contenuto che necessita di componenti strutturati oltre al testo ricco standard.
Definire blocchi personalizzati nei contenuti seed
Usa un _type con namespace nei contenuti Portable Text del 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."
}
]
}
]
}
}
]
}
}
Creare 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>
Renderizzare blocchi personalizzati
Passa i 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 }} />
Poi usalo nelle tue pagine:
---
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 devono essere linkabili:
{
"_type": "marketing.features",
"_key": "features",
"headline": "Features"
}
Poi usalo come ancora nel tuo componente:
<section id={value._key}>
<!-- contenuto -->
</section>
Questo abilita link di navigazione come /#features.
Checklist del tema
Prima di pubblicare, verifica che il tuo tema includa:
-
package.jsoncon campoemdash(label, description, percorso seed) -
.emdash/seed.jsoncon schema valido - Tutte le collezioni referenziate nelle pagine esistono nel seed
- I menu usati nei layout sono definiti nel seed
- I contenuti di esempio dimostrano il design del tema
-
astro.config.mjscon configurazione database e storage -
src/live.config.tscon loader EmDash - Nessun
getStaticPaths()nelle pagine di contenuto - Nessun titolo sito, tagline o navigazione codificata
- Campi immagine accessibili come oggetti (
image.src), non stringhe - README con istruzioni di configurazione
- Componenti blocco personalizzati per qualsiasi tipo Portable Text non standard
Passi successivi
- Formato file seed — Riferimento completo per i file seed
- Panoramica sui temi — Come funzionano i temi in EmDash
- Portare temi WordPress — Convertire temi WordPress esistenti