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.
Conceitos-Chave
Seção intitulada “Conceitos-Chave”- 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 usargetStaticPaths, 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.
Estrutura do Projeto
Seção intitulada “Estrutura do Projeto”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 assetsAs 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.
Configurando package.json
Seção intitulada “Configurando package.json”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" }}| Campo | Descrição |
|---|---|
emdash.label | Nome de exibição mostrado nos seletores de tema |
emdash.description | Breve descrição do tema |
emdash.seed | Caminho para o arquivo de seed |
emdash.preview | URL para uma demonstração ao vivo (opcional) |
O Modelo de Conteúdo Padrão
Seção intitulada “O Modelo de Conteúdo Padrão”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.
Arquivo de Seed
Seção intitulada “Arquivo de Seed”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.
Construindo Páginas
Seção intitulada “Construindo Páginas”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.
Página Inicial
Seção intitulada “Página Inicial”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>Post Único
Seção intitulada “Post Único”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.
Arquivo de Categoria
Seção intitulada “Arquivo de Categoria”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>Usando Imagens
Seção intitulada “Usando Imagens”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>Usando Menus
Seção intitulada “Usando Menus”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>Modelos de Página
Seção intitulada “Modelos de Página”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.
Adicionando Seções
Seção intitulada “Adicionando Seções”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.
Adicionando Conteúdo de Exemplo
Seção intitulada “Adicionando Conteúdo de Exemplo”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"] } } ] }}Incluindo Mídia
Seção intitulada “Incluindo Mídia”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).
Testando Seu Tema
Seção intitulada “Testando Seu Tema”-
Crie um projeto de teste a partir do seu tema:
Terminal window npm create astro@latest -- --template ./path/to/my-theme -
Instale as dependências e inicie o servidor de desenvolvimento:
Terminal window cd test-sitenpm installnpm run dev -
Complete o Assistente de Configuração em
http://localhost:4321/_emdash/admin -
Verifique se as coleções, menus, redirecionamentos e conteúdo foram criados corretamente
-
Teste se todos os modelos de página são renderizados corretamente
-
Crie novo conteúdo através do administrador para verificar se todos os campos funcionam
Publicando Seu Tema
Seção intitulada “Publicando Seu Tema”Publique no npm para distribuição:
npm publish --access publicOs usuários podem então instalar seu tema:
npm create astro@latest -- --template @your-org/emdash-theme-blogPara temas hospedados no GitHub:
npm create astro@latest -- --template github:your-org/emdash-theme-blogBlocos de Texto Portátil Personalizados
Seção intitulada “Blocos de Texto Portátil Personalizados”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." } ] } ] } } ] }}Criando Componentes de Bloco
Seção intitulada “Criando Componentes de Bloco”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>Renderizando Blocos Personalizados
Seção intitulada “Renderizando Blocos Personalizados”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} />IDs de Âncora para Navegação
Seção intitulada “IDs de Âncora para Navegação”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.
Lista de Verificação do Tema
Seção intitulada “Lista de Verificação do Tema”Antes de publicar, verifique se seu tema inclui:
-
package.jsoncom campoemdash(rótulo, descrição, caminho do seed) -
.emdash/seed.jsoncom 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.mjscom configuração de banco de dados e armazenamento -
src/live.config.tscom 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
Próximos Passos
Seção intitulada “Próximos Passos”- Formato do Arquivo de Seed — Referência completa para arquivos de seed
- Visão Geral dos Temas — Como os temas funcionam no EmDash
- Convertendo Temas do WordPress — Converta temas existentes do WordPress