Creare Temi

In questa pagina

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

  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 l’Assistente di Configurazione su http://localhost:4321/_emdash/admin

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

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

  6. 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.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
  • Il contenuto di esempio dimostra 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 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