Pular para o conteúdo

Criando Temas

Um tema EmDash é um site Astro completo — páginas, layouts, componentes, estilos — que também inclui um arquivo de seed para inicializar o modelo de conteúdo. Crie um para compartilhar seu design com outros ou para padronizar a criação de sites para sua agência.

  • Um tema é um projeto Astro funcional. Não há uma API de tema ou camada de abstração. Você constrói um site e o entrega como um template. O arquivo de seed apenas informa ao EmDash quais coleções, campos, menus, redirecionamentos e taxonomias criar na primeira execução.
  • O EmDash oferece mais controle sobre o modelo de conteúdo que o WordPress. Os temas aproveitam isso — o arquivo de seed declara exatamente quais campos cada coleção precisa. Construa sobre as coleções padrão posts e pages e adicione campos e taxonomias conforme seu design exige, em vez de inventar tipos de conteúdo totalmente novos.
  • As páginas de conteúdo do tema devem ser renderizadas no servidor. Em um tema, o conteúdo muda em tempo de execução através da interface administrativa, então as páginas que exibem conteúdo do EmDash não devem ser pré-renderizadas. Não use getStaticPaths() nas rotas de conteúdo do tema. (Builds de sites estáticos usando o EmDash como fonte de dados em tempo de build podem usar getStaticPaths, mas os temas são sempre SSR.)
  • Nenhum conteúdo embutido no código. Título do site, slogan, navegação e outros conteúdos dinâmicos vêm do CMS via chamadas de API — não de strings de template.

Crie um tema com esta estrutura:

my-emdash-theme/
├── package.json # Theme metadata
├── astro.config.mjs # Astro + EmDash configuration
├── src/
│ ├── live.config.ts # Live Collections setup
│ ├── pages/
│ │ ├── index.astro # Homepage
│ │ ├── [...slug].astro # Pages (catch-all)
│ │ ├── posts/
│ │ │ ├── index.astro # Post archive
│ │ │ └── [slug].astro # Single post
│ │ ├── categories/
│ │ │ └── [slug].astro # Category archive
│ │ ├── tags/
│ │ │ └── [slug].astro # Tag archive
│ │ ├── search.astro # Search page
│ │ └── 404.astro # Not found
│ ├── layouts/
│ │ └── Base.astro # Base layout
│ └── components/ # Your components
├── .emdash/
│ ├── seed.json # Schema and sample content
│ └── uploads/ # Optional local media files
└── public/ # Static assets

As páginas ficam na raiz como uma rota catch-all ([...slug].astro), então uma página com slug about é renderizada em /about. Posts, categorias e tags têm seus próprios diretórios. O diretório .emdash/ contém o arquivo de seed e quaisquer arquivos de mídia locais usados no conteúdo de exemplo.

Adicione o campo emdash ao seu package.json:

json title="package.json"
{
"name": "@your-org/emdash-theme-blog",
"version": "1.0.0",
"description": "Um tema de blog minimalista para EmDash",
"keywords": ["astro-template", "emdash", "blog"],
"emdash": {
"label": "Blog minimalista",
"description": "Um blog limpo e minimalista com posts, páginas e categorias",
"seed": ".emdash/seed.json",
"preview": "https://your-theme-demo.pages.dev"
}
}
CampoDescrição
emdash.labelNome de exibição mostrado nos seletores de tema
emdash.descriptionBreve descrição do tema
emdash.seedCaminho para o arquivo de seed
emdash.previewURL para uma demonstração ao vivo (opcional)

A maioria dos temas precisa de dois tipos de coleção: posts e pages. Posts são entradas com data e hora, com excertos e imagens destacadas que aparecem em feeds e arquivos. Pages são conteúdos independentes em URLs de nível superior.

Este é o ponto de partida recomendado. Adicione mais coleções, taxonomias ou campos conforme seu tema precisar, mas comece aqui.

O arquivo de seed informa ao EmDash o que criar na primeira execução. Crie .emdash/seed.json:

json title=".emdash/seed.json"
{
"$schema": "https://emdashcms.com/seed.schema.json",
"version": "1",
"meta": {
"name": "Blog minimalista",
"description": "Um blog limpo com posts e páginas",
"author": "Seu nome"
},
"settings": {
"title": "Meu blog",
"tagline": "Ideias e reflexões",
"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": "Categorias",
"labelSingular": "Categoria",
"hierarchical": true,
"collections": ["posts"],
"terms": [
{ "slug": "news", "label": "Notícias" },
{ "slug": "tutorials", "label": "Tutoriais" }
]
}
],
"menus": [
{
"name": "primary",
"label": "Navegação principal",
"items": [
{ "type": "custom", "label": "Início", "url": "/" },
{ "type": "custom", "label": "Blog", "url": "/posts" }
]
}
],
"redirects": [
{ "source": "/category/news", "destination": "/categories/news" },
{ "source": "/old-about", "destination": "/about" }
]
}

Posts recebem excerpt e featured_image porque aparecem em listas e feeds. Pages não precisam deles — são conteúdos independentes. Adicione campos a qualquer coleção conforme seu tema exigir.

Veja Formato do Arquivo de Seed para a especificação completa, incluindo seções, áreas de widgets e referências de mídia.

Todas as páginas que exibem conteúdo do EmDash são renderizadas no servidor. Use Astro.params para obter o slug da URL e consultar o conteúdo no momento da requisição.

astro title="src/pages/index.astro"
---
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="Início">
<h1>Posts mais recentes</h1>
{posts.map((post) => (
<article>
<h2><a href={`/posts/${post.slug}`}>{post.data.title}</a>
</h2>
<p>{post.data.excerpt}</p>
</article>
))}
</Base>
astro title="src/pages/posts/[slug].astro"
---
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>

Pages usam uma rota catch-all na raiz para que seus slugs mapeiem diretamente para URLs de nível superior — uma página com slug about é renderizada em /about:

astro title="src/pages/[...slug].astro"
---
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 esta é uma rota catch-all, ela só corresponde a URLs que não têm uma rota mais específica. /posts/hello-world ainda acessa posts/[slug].astro, não este arquivo.

astro title="src/pages/categories/[slug].astro"
---
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>

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:

astro title="src/components/PostCard.astro"
---
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>

Consulte menus definidos pelo administrador em seus layouts. Nunca codifique links de navegação de forma fixa:

astro title="src/layouts/Base.astro"
---
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>

Temas frequentemente precisam de múltiplos layouts de página — um layout padrão, um layout de largura total, um layout de página de destino. No EmDash, adicione um campo de seleção template à coleção de páginas e mapeie-o para componentes de layout na sua rota catch-all.

Adicione o campo à sua coleção de páginas no arquivo de 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"
}

Em seguida, mapeie o valor para componentes de layout na rota catch-all:

astro title="src/pages/[...slug].astro"
---
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 modelo a partir de um menu suspenso na interface administrativa ao editar uma página.

Seções são blocos de conteúdo reutilizáveis que os editores podem inserir em qualquer campo de Texto Portátil usando o comando de barra /section. Se o seu tema tem padrões de conteúdo comuns (banners hero, CTAs, grades de recursos), defina-os como seções no arquivo de seed:

json title=".emdash/seed.json"
{
"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": "Bem-vindo ao nosso 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."
}
]
}
]
}
]
}

Seções criadas a partir do arquivo de seed são marcadas com source: "theme". Os editores também podem criar suas próprias seções (marcadas source: "user"), mas as seções fornecidas pelo tema não podem ser excluídas da interface administrativa.

Inclua conteúdo de exemplo no arquivo de seed para demonstrar o design do seu tema:

json title=".emdash/seed.json"
{
"content": {
"posts": [
{
"id": "hello-world",
"slug": "hello-world",
"status": "published",
"data": {
"title": "Ola mundo",
"content": [
{
"_type": "block",
"style": "normal",
"children": [{ "_type": "span", "text": "Welcome to your new blog!" }]
}
],
"excerpt": "Your first post on EmDash."
},
"taxonomies": {
"category": ["news"]
}
}
]
}
}

Referencie imagens no conteúdo de exemplo usando a sintaxe $media.

Para imagens remotas:

{
"data": {
"featured_image": {
"$media": {
"url": "https://images.unsplash.com/photo-xxx",
"alt": "A descriptive alt text",
"filename": "hero.jpg"
}
}
}
}

Para imagens locais, coloque os arquivos em .emdash/uploads/ e referencie-os:

{
"data": {
"featured_image": {
"$media": {
"file": "hero.jpg",
"alt": "A descriptive alt text"
}
}
}
}

Durante a semeadura, os arquivos de mídia são baixados (ou lidos localmente) e enviados para o armazenamento.

Se o seu tema inclui uma página de busca, use o componente LiveSearch para resultados instantâneos:

astro title="src/pages/search.astro"
---
import LiveSearch from "emdash/ui/search";
import Base from "../../layouts/Base.astro";
---
<Base title="Search">
<h1>Busca</h1>
<LiveSearch
placeholder="Buscar posts e páginas..."
collections={["posts", "pages"]}
/>
</Base>

LiveSearch fornece busca instantânea com debounce, correspondência por prefixo, stemming Porter e snippets de resultados destacados. A busca deve ser habilitada por coleção na interface administrativa (Tipos de Conteúdo > Editar > Recursos > Busca).

  1. Crie um projeto de teste a partir do seu tema:

    Terminal window
    npm create astro@latest -- --template ./path/to/my-theme
  2. Instale as dependências e inicie o servidor de desenvolvimento:

    Terminal window
    cd test-site
    npm install
    npm run dev
  3. Complete o Assistente de Configuração em http://localhost:4321/_emdash/admin

  4. Verifique se as coleções, menus, redirecionamentos e conteúdo foram criados corretamente

  5. Teste se todos os modelos de página são renderizados corretamente

  6. Crie novo conteúdo através do administrador para verificar se todos os campos funcionam

Publique no npm para distribuição:

Terminal window
npm publish --access public

Os usuários podem então instalar seu tema:

Terminal window
npm create astro@latest -- --template @your-org/emdash-theme-blog

Para temas hospedados no GitHub:

Terminal window
npm create astro@latest -- --template github:your-org/emdash-theme-blog

Temas podem definir tipos de blocos de Texto Portátil personalizados para conteúdo especializado. Isso é útil para páginas de marketing, páginas de destino ou qualquer conteúdo que precise de componentes estruturados além do texto rico padrão.

Definindo Blocos Personalizados no Conteúdo de Seed

Seção intitulada “Definindo Blocos Personalizados no Conteúdo de Seed”

Use um _type com namespace no conteúdo de Texto Portátil do seu arquivo de seed:

json title=".emdash/seed.json"
{
"content": {
"pages": [
{
"id": "home",
"slug": "home",
"status": "published",
"data": {
"title": "Início",
"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."
}
]
}
]
}
}
]
}
}

Crie componentes Astro para cada tipo de bloco personalizado:

astro title="src/components/blocks/Hero.astro"
---
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>

Passe seus componentes de bloco personalizados para o componente PortableText:

astro title="src/components/MarketingBlocks.astro"
---
import { PortableText } from "emdash/ui";
import Hero from "../../themes/blocks/Hero.astro";
import Features from "../../themes/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 }} />

Em seguida, use-o em suas páginas:

astro title="src/pages/index.astro"
---
import { getEmDashEntry } from "emdash";
import MarketingBlocks from "../../components/MarketingBlocks.astro";
const { entry: page } = await getEmDashEntry("pages", "home");
---
<MarketingBlocks value={page.data.content} />

Adicione _key a blocos que devem ser linkáveis:

{
"_type": "marketing.features",
"_key": "features",
"headline": "Features"
}

Em seguida, use-o como uma âncora em seu componente:

<section id={value._key}>
<!-- content -->
</section>

Isso permite links de navegação como /#features.

Antes de publicar, verifique se seu tema inclui:

  • package.json com campo emdash (rótulo, descrição, caminho do seed)
  • .emdash/seed.json com esquema válido
  • Todas as coleções referenciadas nas páginas existem no seed
  • Menus usados nos layouts estão definidos no seed
  • O conteúdo de exemplo demonstra o design do tema
  • astro.config.mjs com configuração de banco de dados e armazenamento
  • src/live.config.ts com carregador do EmDash
  • Sem getStaticPaths() em páginas de conteúdo
  • Sem título do site, slogan ou navegação codificados de forma fixa
  • Campos de imagem acessados como objetos (image.src), não strings
  • README com instruções de configuração
  • Componentes de bloco personalizados para quaisquer tipos de Texto Portátil não padrão