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 temi né un livello di astrazione. Un tema è un sito impacchettato come template. Il file seed indica a EmDash quali collezioni, campi, menu, redirect 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 a runtime attraverso l’interfaccia di amministrazione, quindi le pagine che mostrano contenuto EmDash non devono essere pre-renderizzate. Non usare
getStaticPaths()nelle rotte di contenuto del tema. (Le build di siti statici che usano EmDash come fonte dati al build possono usaregetStaticPaths, ma i temi sono sempre SSR.) - Nessun contenuto hardcoded. Il titolo del sito, tagline, navigazione e altro contenuto dinamico provengono dal CMS tramite chiamate API — non da stringhe di template.
Struttura del progetto
Un tema usa 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 categoria
│ │ ├── 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 sono posizionate alla radice come rotta catch-all ([...slug].astro), così una pagina con slug about viene renderizzata a /about. Post, categorie e tag ottengono le proprie directory. La directory .emdash/ contiene il file seed e qualsiasi file multimediale locale usato nel contenuto 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 di temi |
emdash.description | Breve descrizione del tema |
emdash.seed | Percorso al file seed |
emdash.preview | URL a 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 timestamp con estratti e immagini in evidenza che appaiono in feed e archivi. Le pagine sono contenuto autonomo su URL di primo livello.
Questo è il punto di partenza raccomandato. Aggiungi altre collezioni, tassonomie o campi secondo le necessità del tuo tema, ma inizia da qui.
File Seed
Il file seed indica 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 liste e feed. Le pagine non ne hanno bisogno — sono contenuto autonomo. Aggiungi campi a qualsiasi collezione secondo le necessità del tuo tema.
Consulta Formato file Seed per la specifica completa, incluse sezioni, aree widget e riferimenti multimediali.
Costruire pagine
Tutte le pagine che mostrano contenuto EmDash sono renderizzate lato server. Usa Astro.params per ottenere lo slug dall’URL e interrogare il contenuto ad ogni 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>
Pagine
Le pagine usano una rotta catch-all alla radice così i loro slug corrispondono direttamente a 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>
Essendo una rotta catch-all, corrisponde solo a URL senza una rotta più specifica. /posts/hello-world raggiunge ancora posts/[slug].astro, non questo file.
Archivio categoria
---
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 di 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}
priority
/>
)}
<h2><a href={`/posts/${post.slug}`}>{post.data.title}</a></h2>
<p>{post.data.excerpt}</p>
</article>
Usa priority solo per l’immagine probabilmente visibile above the fold in una pagina, come un hero o la prima immagine di una card. Renderizza l’immagine con loading="eager" e fetchpriority="high". loading controlla se il browser può differire il caricamento, mentre fetchpriority indica quanto sia importante la richiesta una volta che il browser la scopre.
Usare i menu
Interroga i menu definiti dall’admin nei tuoi layout. Non hardcodare 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 layout multipli — un layout predefinito, uno a larghezza piena, un layout per landing page. In EmDash, aggiungi un campo select template alla collezione pagine e mappalo ai componenti di layout nella tua rotta 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 di layout nella rotta 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’interfaccia di amministrazione 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 ricorrenti (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 editor possono anche creare le proprie sezioni (contrassegnate source: "user"), ma le sezioni fornite dal tema non possono essere eliminate dall’interfaccia di amministrazione.
Aggiungere 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"]
}
}
]
}
}
Includere media
Referenzia immagini nel contenuto di esempio usando la sintassi $media. Un’immagine remota viene referenziata 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 referenziali 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, corrispondenza di prefissi, stemming Porter e snippet di risultati evidenziati. La ricerca deve essere abilitata per collezione nell’interfaccia di amministrazione (Content Types > Edit > Features > Search).
Testare il 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 server di sviluppo:
cd test-site npm install npm run dev -
Completa l’Assistente di Configurazione su
http://localhost:4321/_emdash/admin -
Verifica che collezioni, menu, redirect e contenuto siano stati creati correttamente
-
Testa che tutti i template di pagina vengano renderizzati correttamente
-
Crea nuovo contenuto tramite l’admin per verificare che tutti i campi funzionino
Pubblicare il tuo tema
Pubblica su npm per la distribuzione:
npm publish --access public
Gli utenti possono poi installare il tuo tema:
npm create astro@latest -- --template @your-org/emdash-theme-blog
Un tema ospitato su GitHub si installa 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 di componenti strutturati oltre il rich text standard.
Definire 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."
}
]
}
]
}
}
]
}
}
Creare componenti di 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 tuoi componenti di 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 devono essere linkabili:
{
"_type": "marketing.features",
"_key": "features",
"headline": "Features"
}
Usa il valore _key come ancora nel componente di blocco:
<section id={value._key}>
<!-- contenuto -->
</section>
Questo permette 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
- Il contenuto di esempio dimostra 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 del sito, tagline o navigazione hardcoded
- Campi immagine acceduti come oggetti (
image.src), non stringhe - README con istruzioni di configurazione
- Componenti di blocco personalizzati per tipi Portable Text non standard
Prossimi passi
- Formato file Seed — Riferimento completo per i file seed
- Panoramica dei Temi — Come funzionano i temi in EmDash
- Portare Temi WordPress — Convertire temi WordPress esistenti