Um tema EmDash é um projeto Astro que outro desenvolvedor pode scaffoldar com create-astro. Construa e teste o projeto como um site completo e inclua um seed que cria o modelo de conteúdo esperado pelas rotas e componentes.
Começar a partir de um modelo atual
Escolha o modelo existente mais próximo do site pretendido e do alvo de implantação. As variantes Node e Cloudflare mantêm juntos banco de dados, armazenamento, adaptador e configuração de middleware.
O modelo de blog é uma base útil para um site de conteúdo:
npm create astro@latest -- --template @emdash-cms/template-blog
Use @emdash-cms/template-blog-cloudflare quando o tema resultante tiver como alvo Cloudflare Workers, D1 e R2.
Manter a estrutura do modelo
O modelo de blog atual usa estes caminhos relevantes:
astro.config.mjs
emdash-env.d.ts
package.json
seed/
└── seed.json
src/
├── components/
│ └── PostCard.astro
├── layouts/
│ └── Base.astro
├── live.config.ts
├── pages/
│ ├── index.astro
│ ├── category/[slug].astro
│ ├── pages/[slug].astro
│ ├── posts/index.astro
│ ├── posts/[slug].astro
│ ├── search.astro
│ └── tag/[slug].astro
└── styles/
Os modelos starter, portfolio e marketing usam rotas diferentes. Copie os arquivos reais da base escolhida em vez de assumir que todo tema tem uma rota catch-all de páginas.
Apontar para o seed
Os modelos atuais declaram o caminho do seed em package.json:
{
"name": "@example/emdash-theme-publication",
"private": true,
"type": "module",
"emdash": {
"seed": "seed/seed.json"
}
}
O EmDash também descobre .emdash/seed.json e o fallback convencional seed/seed.json. Use o campo do pacote ao distribuir um modelo para que o arquivo pretendido fique explícito.
Definir o modelo de conteúdo
Se você partir do modelo de blog, edite o seed/seed.json existente em vez de substituí-lo por um modelo não relacionado. O seed reduzido a seguir mantém as coleções e os dados estruturais usados nos exemplos deste guia:
{
"$schema": "https://emdashcms.com/seed.schema.json",
"version": "1",
"meta": {
"name": "Publication",
"description": "A publication with posts"
},
"settings": {
"title": "Publication",
"tagline": "Latest articles"
},
"collections": [
{
"slug": "posts",
"label": "Posts",
"labelSingular": "Post",
"supports": ["drafts", "revisions", "search", "seo"],
"fields": [
{
"slug": "title",
"label": "Title",
"type": "string",
"required": true,
"searchable": true
},
{
"slug": "excerpt",
"label": "Excerpt",
"type": "text"
},
{
"slug": "featured_image",
"label": "Featured image",
"type": "image"
},
{
"slug": "content",
"label": "Content",
"type": "portableText",
"searchable": true
}
]
},
{
"slug": "pages",
"label": "Pages",
"labelSingular": "Page",
"supports": ["drafts", "revisions", "search"],
"fields": [
{
"slug": "title",
"label": "Title",
"type": "string",
"required": true,
"searchable": true
},
{
"slug": "content",
"label": "Content",
"type": "portableText",
"searchable": true
},
{
"slug": "template",
"label": "Page template",
"type": "select",
"defaultValue": "default",
"validation": {
"options": ["default", "full-width", "landing"]
}
}
]
}
],
"menus": [
{
"name": "primary",
"label": "Primary navigation",
"items": [
{ "type": "custom", "label": "Home", "url": "/" },
{ "type": "custom", "label": "Posts", "url": "/posts" }
]
}
],
"widgetAreas": [
{
"name": "sidebar",
"label": "Sidebar",
"widgets": []
}
],
"content": {
"posts": [
{
"id": "post-welcome",
"slug": "welcome",
"status": "published",
"data": {
"title": "Welcome",
"excerpt": "The first article",
"content": []
}
}
]
}
}
O id de uma entrada de conteúdo é um identificador local ao seed usado por referências. Não é obrigado a virar o ID do banco de uma entrada roteável. O slug torna-se o identificador de rota exposto como entry.id pela API de consulta.
Leia Arquivos seed antes de adicionar taxonomias, bylines, referências de menu, mídia, redirecionamentos, áreas de widgets, sections, localização ou comportamento de conflito.
Construir rotas renderizadas no servidor
Os modelos EmDash atuais usam output: "server". As rotas consultam conteúdo ao vivo a cada requisição. Não adicione getStaticPaths() às rotas de conteúdo de um tema, a menos que o modelo use o EmDash deliberadamente apenas como fonte em tempo de build.
O arquivo a seguir ordena no banco usando o campo armazenado published_at:
---
import { getEmDashCollection } from "emdash";
import Base from "../../layouts/Base.astro";
const { entries: posts, error, cacheHint } = await getEmDashCollection("posts", {
orderBy: { published_at: "desc" },
});
if (error) return new Response("Could not load posts", { status: 500 });
if (Astro.cache?.enabled) Astro.cache.set(cacheHint);
---
<Base title="Posts">
{posts.map((post) => (
<article>
<h2><a href={`/posts/${post.id}`}>{post.data.title}</a></h2>
{post.data.excerpt && <p>{post.data.excerpt}</p>}
</article>
))}
</Base>
orderBy é um objeto de campo para direção. Use orderBy: { published_at: "desc" }, não sort, sortBy nem um callback JavaScript.
A rota a seguir resolve e renderiza uma publicação:
---
import { decodeSlug, getEmDashEntry } from "emdash";
import { PortableText } from "emdash/ui";
import Base from "../../layouts/Base.astro";
const slug = decodeSlug(Astro.params.slug);
if (!slug) return Astro.redirect("/404");
const { entry: post, error, cacheHint } = await getEmDashEntry("posts", slug);
if (error) return new Response("Could not load the post", { status: 500 });
if (!post) return Astro.redirect("/404");
if (Astro.cache?.enabled) Astro.cache.set(cacheHint);
---
<Base title={post.data.title} content={{ collection: "posts", id: post.data.id, slug }}>
<article>
<h1 {...post.edit.title}>{post.data.title}</h1>
<PortableText value={post.data.content} />
</article>
</Base>
Use post.id nas URLs de rota e post.data.id onde um helper exige o ID de conteúdo armazenado.
Consultar navegação gerenciada pelo site
Valores gerenciados pelo CMS devem vir das APIs correspondentes. Os modelos atuais usam getSiteSettings(), getMenu() e <WidgetArea /> nos layouts:
---
import { getMenu, getSiteSettings } from "emdash";
import { WidgetArea } from "emdash/ui";
const [settings, primary] = await Promise.all([
getSiteSettings(),
getMenu("primary"),
]);
---
<header>
<a href="/">{settings.title}</a>
<nav>
{primary?.items.map((item) => <a href={item.url}>{item.label}</a>)}
</nav>
</header>
<main><slot /></main>
<aside><WidgetArea name="sidebar" /></aside>
Texto de design estático pode permanecer em arquivos Astro. Valores que administradores devem editar precisam estar representados em configurações, conteúdo, menus ou widgets.
Renderizar campos de imagem
Campos de imagem são valores de mídia, não strings de URL. Passe o valor completo ao componente Image para que armazenamento local e provedores de imagem resolvam de forma consistente:
---
import { Image } from "emdash/ui";
const { post } = Astro.props;
---
<article>
{post.data.featured_image && (
<Image
image={post.data.featured_image}
alt={post.data.title}
width={800}
height={450}
/>
)}
<h2><a href={`/posts/${post.id}`}>{post.data.title}</a></h2>
</article>
O componente usa o texto alternativo do próprio campo, salvo se você substituir. Reserve priority para uma imagem esperada above the fold; as demais permanecem com lazy load.
Oferecer opções de layout de página
O seed inicial acima inclui um campo select para sites em que editores precisam de mais de um layout de página. Ao adicionar o recurso a outro seed, use valores estáveis que mapeiem para componentes conhecidos:
{
"slug": "template",
"label": "Page template",
"type": "select",
"defaultValue": "default",
"validation": {
"options": ["default", "full-width", "landing"]
}
}
A rota pode então escolher de um mapa explícito de componentes:
---
import { decodeSlug, getEmDashEntry } from "emdash";
import PageDefault from "../../layouts/PageDefault.astro";
import PageFullWidth from "../../layouts/PageFullWidth.astro";
import PageLanding from "../../layouts/PageLanding.astro";
const slug = decodeSlug(Astro.params.slug);
if (!slug) return Astro.redirect("/404");
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} />
O mapa explícito impede que um valor de campo armazenado vire um caminho de módulo arbitrário.
Adicionar busca
Habilite search em cada coleção que deve aparecer nos resultados e marque os campos relevantes como searchable. Os modelos atuais usam LiveSearch para uma rota de busca pronta:
---
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>
Em um site Astro i18n, LiveSearch usa Astro.currentLocale. Passe locale={null} somente quando a página buscar intencionalmente todos os locales.
Seed de sections reutilizáveis
Sections dão aos editores pontos de partida Portable Text reutilizáveis. Adicione-as quando o design incluir um padrão de conteúdo repetido, como uma call to action:
{
"version": "1",
"sections": [
{
"slug": "newsletter-signup",
"title": "Newsletter signup",
"description": "Heading and copy for the newsletter form",
"keywords": ["newsletter", "email"],
"content": [
{
"_type": "block",
"_key": "newsletter-heading",
"style": "h2",
"children": [
{ "_type": "span", "_key": "newsletter-heading-text", "text": "Get new articles by email" }
]
}
]
}
]
}
O assistente de configuração permite omitir entradas seed, bylines e termos de taxonomia. Sections e o restante do modelo estrutural ainda são aplicados.
Adicionar blocks de página estruturados
Use um campo blocks quando a própria página for uma composição ordenada de seções tipadas. Use um block Portable Text personalizado quando o objeto pertencer a um documento rich text e participar do fluxo de parágrafos.
O modelo marketing faz seed de cinco tipos de block retained e os permite no campo content de Pages. O wrapper mapeia cada _type gerado para um componente Astro:
---
import { Blocks, defineBlockComponents } from "emdash/ui";
import type { PageContentBlock } from "../../emdash-env";
import FAQ from "./blocks/FAQ.astro";
import Features from "./blocks/Features.astro";
import Hero from "./blocks/Hero.astro";
import Pricing from "./blocks/Pricing.astro";
import Testimonials from "./blocks/Testimonials.astro";
const components = defineBlockComponents<PageContentBlock>({
marketing_hero: Hero,
marketing_features: Features,
marketing_testimonials: Testimonials,
marketing_pricing: Pricing,
marketing_faq: FAQ,
});
---
<Blocks value={Astro.props.value} components={components} />
A união PageContentBlock gerada mantém o mapa de componentes alinhado a cada tipo de block permitido e versão retained. Cada valor de block no seed inclui _type, _version e uma _key estável.
Use um block Portable Text personalizado para um objeto entre parágrafos, como um diagrama embutido em um artigo. Mapeie esse objeto pela opção components.type do componente PortableText. Um seed pode incluir o valor armazenado, mas um plugin nativo deve registrar um editor personalizado para criar e editar esse objeto Portable Text.
Use um plugin nativo quando o block precisar de um editor personalizado reutilizável e componentes de renderização empacotados. Plugins em sandbox não podem incluir componentes de renderização Astro no build do site.
Testar o modelo
-
Scaffolde o modelo em um diretório limpo com
create-astro. -
Instale dependências e execute os comandos de build e verificação de tipos do site.
-
Comece com um banco de dados vazio e conclua
/_emdash/admin/setup. -
Teste a configuração com conteúdo de exemplo incluído e excluído.
-
Abra cada rota com conteúdo seed, conteúdo recém-criado, sem imagem opcional e com coleção vazia.
-
Edite configurações do site, menus, atribuições de taxonomia, Portable Text e blocks pelo admin. Para blocks, adicione, reordene, duplique e remova cada tipo permitido. Confirme que as rotas públicas usam os valores alterados.
-
Aplique a configuração específica de implantação em um ambiente descartável e verifique armazenamento de mídia, previews e renderização no servidor.
Publicar o modelo
Um repositório GitHub pode ser usado diretamente com a sintaxe de modelo github: do Astro:
npm create astro@latest -- --template github:example/emdash-theme-publication
Antes de publicar, remova bancos de dados e uploads locais, mantenha segredos fora do repositório, verifique caminhos de pacotes a partir de um checkout limpo e documente qual alvo de implantação o modelo configura.
Lista de verificação
-
astro.config.mjsusa saída de servidor e os adaptadores de implantação pretendidos. -
src/live.config.tsregistraemdashLoader(). -
package.json#emdash.seedaponta para um arquivo existente. - Toda coleção e campo consultados estão declarados no seed.
- Exemplos de consulta usam
orderBye o identificador de entrada correto. - Valores do site gerenciados pelo CMS não estão duplicados como constantes permanentes do modelo.
- Uma configuração limpa funciona com e sem conteúdo de exemplo.
- Build e verificação de tipos do modelo passam para o alvo suportado.