Criar um tema

Nesta página

Um tema EmDash é um projeto Astro que outro desenvolvedor pode gerar com create-astro. Construa e teste o projeto como um site completo e, em seguida, inclua um seed que crie o modelo de conteúdo esperado por suas rotas e componentes.

Começar a partir de um modelo atual

Escolha o modelo existente mais próximo do site pretendido e do destino de implantação. As variantes Node e Cloudflare mantêm juntas a configuração de banco de dados, armazenamento, adaptador e 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 for destinado a 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 presumir que todo tema tem uma rota de página genérica (catch-all).

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 seja explícito.

Definir o modelo de conteúdo

Ao começar a partir do modelo de blog, edite o seed/seed.json existente em vez de substituí-lo por um modelo sem relação. O seed reduzido a seguir mantém as collections e os dados estruturais usados pelos 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 da entrada de conteúdo é um identificador local do seed usado pelas referências. Ele não é forçado a se tornar o ID de banco de dados de uma entrada roteável. O slug dela se torna 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 widget, sections, localização ou comportamento de conflito.

Construir rotas renderizadas no servidor

Os modelos EmDash atuais usam output: "server". As rotas consultam o conteúdo ativo a cada requisição. Não adicione getStaticPaths() às rotas de conteúdo de um tema, a menos que o modelo use deliberadamente o EmDash apenas como fonte em tempo de build.

O arquivo a seguir ordena no banco de dados 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" }, e não sort, sortBy ou uma função de callback em JavaScript.

A rota a seguir resolve e renderiza um post:

---
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.rewrite("/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.rewrite("/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 das rotas e post.data.id quando um helper exigir o ID de conteúdo armazenado.

Consultar navegação gerenciada pelo site

Os valores gerenciados pelo CMS devem vir das APIs correspondentes. Os modelos atuais usam getSiteSettings(), getMenu() e <WidgetArea /> em seus 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>

O texto estático do design pode permanecer nos arquivos Astro. Os valores que os administradores devem poder editar precisam ser representados em configurações, conteúdo, menus ou widgets.

Renderizar campos de imagem

Os campos de imagem são valores de mídia, não strings de URL. Passe o valor completo ao componente Image para que o armazenamento local e os provedores de imagem sejam resolvidos 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, a menos que você o substitua. Reserve priority para uma imagem que deve aparecer na primeira dobra da página; as demais imagens continuam com carregamento lazy.

Oferecer opções de layout de página

O seed inicial acima inclui um campo select para sites em que os editores precisam de mais de um layout de página. Ao adicionar o recurso a outro seed, use valores estáveis que correspondam a componentes conhecidos:

{
  "slug": "template",
  "label": "Page template",
  "type": "select",
  "defaultValue": "default",
  "validation": {
    "options": ["default", "full-width", "landing"]
  }
}

A rota pode então escolher a partir 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.rewrite("/404");

const { entry: page } = await getEmDashEntry("pages", slug);
if (!page) return Astro.rewrite("/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 se torne um caminho de módulo arbitrário.

Adicionar busca

Habilite search em cada collection que deva 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 com i18n, LiveSearch usa Astro.currentLocale. Passe locale={null} somente quando a página deve pesquisar deliberadamente em todos os locales.

Seed de sections reutilizáveis

As sections oferecem aos editores pontos de partida reutilizáveis em Portable Text. Adicione-as quando o design incluir um padrão de conteúdo repetido, como uma chamada para ação:

{
  "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 que o usuário omita as entradas, os bylines e os termos de taxonomia inseridos pelo seed. As sections e o restante do modelo estrutural continuam sendo aplicados.

Adicionar blocks de página estruturados

Use um campo blocks quando a própria página for uma composição ordenada de sections tipadas. Use um block personalizado de Portable Text quando o objeto pertencer ao interior de um documento de rich text e participar do fluxo de seus parágrafos.

O modelo marketing insere via seed onze tipos de block retidos e os permite no campo content de Pages. Seu wrapper mapeia cada _type gerado para um componente Astro:

---
import { Blocks, defineBlockComponents } from "emdash/ui";
import type { PageContentBlock } from "../../emdash-env";
import CTA from "./blocks/CTA.astro";
import Contact from "./blocks/Contact.astro";
import FAQ from "./blocks/FAQ.astro";
import Features from "./blocks/Features.astro";
import Hero from "./blocks/Hero.astro";
import Logos from "./blocks/Logos.astro";
import Pricing from "./blocks/Pricing.astro";
import Split from "./blocks/Split.astro";
import Stats from "./blocks/Stats.astro";
import Testimonials from "./blocks/Testimonials.astro";
import Text from "./blocks/Text.astro";

const components = defineBlockComponents<PageContentBlock>({
  marketing_hero: Hero,
  marketing_features: Features,
  marketing_testimonials: Testimonials,
  marketing_pricing: Pricing,
  marketing_faq: FAQ,
  marketing_logos: Logos,
  marketing_split: Split,
  marketing_stats: Stats,
  marketing_cta: CTA,
  marketing_contact: Contact,
  marketing_text: Text,
});
---

<Blocks value={Astro.props.value} components={components} />

A união PageContentBlock gerada mantém o mapa de componentes alinhado com cada tipo de block permitido e cada versão retida. Cada valor de block inserido via seed inclui _type, _version e um _key estável.

Use um block personalizado de Portable Text para um objeto que fica entre parágrafos, como um diagrama incorporado em um artigo. Mapeie esse objeto por meio da opção components.type do componente PortableText. Um seed pode incluir o valor armazenado, mas um plugin nativo precisa 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 de componentes de renderização empacotados. Plugins em sandbox não podem incluir componentes de renderização Astro no build de um site.

Testar o modelo

  1. Gere o modelo em um diretório limpo com create-astro.

  2. Instale as dependências e execute os comandos de build e de verificação de tipos do site.

  3. Comece com um banco de dados vazio e conclua /_emdash/admin/setup.

  4. Teste a configuração com e sem o conteúdo de exemplo.

  5. Abra todas as rotas com conteúdo inserido via seed, conteúdo recém-criado, sem a imagem opcional e com uma collection vazia.

  6. Edite as configurações do site, os menus, as atribuições de taxonomia, o Portable Text e os blocks pela administração. Para os blocks, adicione, reordene, duplique e remova cada tipo permitido. Confirme que as rotas públicas usam os valores alterados.

  7. Aplique a configuração específica da implantação em um ambiente descartável e verifique o armazenamento de mídia, as pré-visualizações e a renderização no servidor.

Publicar o modelo

Um repositório do 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 os segredos fora do repositório, verifique os caminhos do pacote a partir de um checkout limpo e documente qual destino de implantação o modelo configura.

Lista de verificação

  • astro.config.mjs usa saída de servidor e os adaptadores de implantação pretendidos.
  • src/live.config.ts registra emdashLoader().
  • package.json#emdash.seed aponta para um arquivo existente.
  • Toda collection e todo campo consultado são declarados pelo seed.
  • Os exemplos de consulta usam orderBy e o identificador de entrada correto.
  • Os valores do site gerenciados pelo CMS não são duplicados como constantes permanentes do modelo.
  • Uma configuração limpa funciona com e sem conteúdo de exemplo.
  • O build e a verificação de tipos do modelo passam para o destino suportado.