Um tema EmDash é um site Astro completo — páginas, layouts, componentes, estilos — que também inclui um ficheiro seed para inicializar o modelo de conteúdo. Crie um para partilhar o seu design com outros, ou para padronizar a criação de sites para a sua agência.
Conceitos-chave
- Um tema é um projeto Astro funcional. Não há API de temas nem camada de abstração. Um tema é um site empacotado como template. O ficheiro seed indica ao EmDash que coleções, campos, menus, redirecionamentos e taxonomias criar na primeira execução.
- O ficheiro seed declara o modelo de conteúdo. Lista exatamente quais campos cada coleção necessita. Construa sobre as coleções padrão posts e pages e adicione campos e taxonomias conforme o design exigir, em vez de inventar tipos de conteúdo completamente novos.
- As páginas de conteúdo do tema devem ser renderizadas no servidor. Num tema, o conteúdo muda em tempo de execução através da interface de administração, portanto as páginas que exibem conteúdo EmDash não devem ser pré-renderizadas. Não use
getStaticPaths()em rotas de conteúdo do tema. (Builds de sites estáticos que usam EmDash como fonte de dados em tempo de build podem usargetStaticPaths, mas temas são sempre SSR.) - Sem conteúdo hardcoded. O título do site, tagline, navegação e outro conteúdo dinâmico vêm do CMS através de chamadas API — não de strings de template.
Estrutura do projeto
Um tema usa a seguinte estrutura:
my-emdash-theme/
├── package.json # Metadados do tema
├── astro.config.mjs # Configuração Astro + EmDash
├── src/
│ ├── live.config.ts # Configuração Live Collections
│ ├── pages/
│ │ ├── index.astro # Página inicial
│ │ ├── [...slug].astro # Páginas (catch-all)
│ │ ├── posts/
│ │ │ ├── index.astro # Arquivo de posts
│ │ │ └── [slug].astro # Post individual
│ │ ├── categories/
│ │ │ └── [slug].astro # Arquivo de categoria
│ │ ├── tags/
│ │ │ └── [slug].astro # Arquivo de tags
│ │ ├── search.astro # Página de pesquisa
│ │ └── 404.astro # Não encontrado
│ ├── layouts/
│ │ └── Base.astro # Layout base
│ └── components/ # Os seus componentes
├── .emdash/
│ ├── seed.json # Schema e conteúdo de exemplo
│ └── uploads/ # Ficheiros multimédia locais opcionais
└── public/ # Assets estáticos
As páginas ficam na raiz como rota catch-all ([...slug].astro), assim uma página com slug about é renderizada em /about. Posts, categorias e tags têm os seus próprios diretórios. O diretório .emdash/ contém o ficheiro seed e quaisquer ficheiros multimédia locais usados no conteúdo de exemplo.
Configurar package.json
Adicione o campo emdash ao seu 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 | Descrição |
|---|---|
emdash.label | Nome de exibição nos seletores de temas |
emdash.description | Breve descrição do tema |
emdash.seed | Caminho para o ficheiro seed |
emdash.preview | URL para uma demo ao vivo (opcional) |
O modelo de conteúdo padrão
A maioria dos temas necessita de dois tipos de coleção: posts e pages. Posts são entradas com timestamp, com excertos e imagens de destaque que aparecem em feeds e arquivos. Pages são conteúdo independente em URLs de nível superior.
Este é o ponto de partida recomendado. Adicione mais coleções, taxonomias ou campos conforme o seu tema necessitar, mas comece aqui.
Ficheiro Seed
O ficheiro seed indica ao EmDash o que criar na primeira execução. Crie .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" }
]
}
Posts obtêm excerpt e featured_image porque aparecem em listas e feeds. Pages não precisam deles — são conteúdo independente. Adicione campos a qualquer coleção conforme o seu tema necessitar.
Consulte Formato do ficheiro Seed para a especificação completa, incluindo secções, áreas de widgets e referências multimédia.
Construir páginas
Todas as páginas que exibem conteúdo EmDash são renderizadas no servidor. Use Astro.params para obter o slug da URL e consultar conteúdo em cada pedido.
Página inicial
---
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 individual
---
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>
Páginas
As páginas usam uma rota catch-all na raiz para que os seus slugs mapeiem diretamente para URLs de nível superior — uma página com slug about é renderizada em /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>
Como é uma rota catch-all, só corresponde a URLs sem uma rota mais específica. /posts/hello-world ainda chega a posts/[slug].astro, não a este ficheiro.
Arquivo de 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>
Usar imagens
Os campos de imagem são objetos com propriedades src e alt, não strings. Use o componente Image de emdash/ui para renderização otimizada de imagens:
---
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>
Use priority apenas para a imagem provavelmente visível acima da dobra numa página, como um hero ou primeira imagem de card. Renderiza a imagem com loading="eager" e fetchpriority="high". loading controla se o navegador pode adiar o carregamento, enquanto fetchpriority indica a importância do pedido após o navegador o descobrir.
Usar menus
Consulte menus definidos pelo admin nos seus layouts. Nunca hardcode links de navegação:
---
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>
Templates de página
Os temas frequentemente precisam de múltiplos layouts — um layout padrão, um de largura total, um layout de landing page. No EmDash, adicione um campo select template à coleção de páginas e mapeie-o a componentes de layout na sua rota catch-all.
Adicione o campo à coleção de páginas no ficheiro 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"
}
Depois mapeie o valor a componentes de layout na rota 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} />
Os editores escolhem o template a partir de um dropdown na interface de administração ao editar uma página.
Adicionar secções
As secções são blocos de conteúdo reutilizáveis que os editores podem inserir em qualquer campo Portable Text usando o comando slash /section. Se o seu tema tem padrões de conteúdo comuns (banners hero, CTAs, grelhas de funcionalidades), defina-os como secções no ficheiro 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."
}
]
}
]
}
]
}
As secções criadas a partir do ficheiro seed são marcadas com source: "theme". Os editores também podem criar as suas próprias secções (marcadas source: "user"), mas as secções fornecidas pelo tema não podem ser eliminadas da interface de administração.
Adicionar conteúdo de exemplo
Inclua conteúdo de exemplo no ficheiro seed para demonstrar o design do seu 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"]
}
}
]
}
}
Incluir multimédia
Referencie imagens no conteúdo de exemplo usando a sintaxe $media. Uma imagem remota é referenciada por URL:
{
"data": {
"featured_image": {
"$media": {
"url": "https://images.unsplash.com/photo-xxx",
"alt": "A descriptive alt text",
"filename": "hero.jpg"
}
}
}
}
Para imagens locais, coloque ficheiros em .emdash/uploads/ e referencie-os pelo nome do ficheiro:
{
"data": {
"featured_image": {
"$media": {
"file": "hero.jpg",
"alt": "A descriptive alt text"
}
}
}
}
Durante o seeding, os ficheiros multimédia são descarregados (ou lidos localmente) e carregados para o armazenamento.
Pesquisa
Se o seu tema inclui uma página de pesquisa, use o componente LiveSearch para resultados instantâneos:
---
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 fornece pesquisa instantânea com debounce, correspondência de prefixos, stemming Porter e snippets de resultados destacados. A pesquisa deve ser ativada por coleção na interface de administração (Content Types > Edit > Features > Search).
Testar o seu tema
-
Crie um projeto de teste a partir do seu tema:
npm create astro@latest -- --template ./path/to/my-theme -
Instale as dependências e inicie o servidor de desenvolvimento:
cd test-site npm install npm run dev -
Complete o Assistente de Configuração em
http://localhost:4321/_emdash/admin -
Verifique que coleções, menus, redirecionamentos e conteúdo foram criados corretamente
-
Teste que todos os templates de página são renderizados corretamente
-
Crie novo conteúdo através do admin para verificar que todos os campos funcionam
Publicar o seu tema
Publique no npm para distribuição:
npm publish --access public
Os utilizadores podem então instalar o seu tema:
npm create astro@latest -- --template @your-org/emdash-theme-blog
Um tema alojado no GitHub é instalado com o prefixo de template github::
npm create astro@latest -- --template github:your-org/emdash-theme-blog
Blocos Portable Text personalizados
Os temas podem definir tipos de blocos Portable Text personalizados para conteúdo especializado. Isto é útil para páginas de marketing, landing pages ou qualquer conteúdo que precise de componentes estruturados além do rich text padrão.
Definir blocos personalizados no conteúdo seed
Use um _type com namespace no conteúdo Portable Text do seu ficheiro 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."
}
]
}
]
}
}
]
}
}
Criar componentes de bloco
Crie componentes Astro para cada tipo de bloco personalizado:
---
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>
Renderizar blocos personalizados
Passe os seus componentes de bloco personalizados ao 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 }} />
Renderize o componente wrapper numa página:
---
import { getEmDashEntry } from "emdash";
import MarketingBlocks from "../components/MarketingBlocks.astro";
const { entry: page } = await getEmDashEntry("pages", "home");
---
<MarketingBlocks value={page.data.content} />
IDs de âncora para navegação
Adicione _key a blocos que devem ser linkáveis:
{
"_type": "marketing.features",
"_key": "features",
"headline": "Features"
}
Use o valor _key como âncora no componente de bloco:
<section id={value._key}>
<!-- conteúdo -->
</section>
Isto permite links de navegação como /#features.
Checklist do tema
Antes de publicar, verifique que o seu tema inclui:
-
package.jsoncom campoemdash(label, description, caminho seed) -
.emdash/seed.jsoncom schema válido - Todas as coleções referenciadas nas páginas existem no seed
- Menus usados nos layouts estão definidos no seed
- Conteúdo de exemplo demonstra o design do tema
-
astro.config.mjscom configuração de base de dados e armazenamento -
src/live.config.tscom loader EmDash - Sem
getStaticPaths()nas páginas de conteúdo - Sem título do site, tagline ou navegação hardcoded
- Campos de imagem acedidos como objetos (
image.src), não strings - README com instruções de configuração
- Componentes de bloco personalizados para tipos Portable Text não padrão
Próximos passos
- Formato do ficheiro Seed — Referência completa para ficheiros seed
- Visão geral de Temas — Como os temas funcionam no EmDash
- Portar Temas WordPress — Converter temas WordPress existentes