Creare temi

In questa pagina

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 usare getStaticPaths, 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"
	}
}
CampoDescrizione
emdash.labelNome visualizzato nei selettori tema
emdash.descriptionBreve descrizione del tema
emdash.seedPercorso del file seed
emdash.previewURL 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

  1. Crea un progetto di test dal tuo tema:

    npm create astro@latest -- --template ./path/to/my-theme
  2. Installa le dipendenze e avvia il server di sviluppo:

    cd test-site
    npm install
    npm run dev
  3. Completa la procedura guidata su http://localhost:4321/_emdash/admin

  4. Verifica che collezioni, menu, redirect e contenuti siano stati creati correttamente

  5. Testa che tutti i template di pagina vengano renderizzati correttamente

  6. 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.json con campo emdash (label, description, percorso seed)
  • .emdash/seed.json con 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.mjs con configurazione database e storage
  • src/live.config.ts con 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