Crear un tema

En esta página

Un tema EmDash es un proyecto Astro que otro desarrollador puede andamiar con create-astro. Construye y prueba el proyecto como un sitio completo e incluye un seed que crea el modelo de contenido que esperan sus rutas y componentes.

Partir de una plantilla actual

Elige la plantilla existente más cercana al sitio previsto y al destino de despliegue. Las variantes Node y Cloudflare mantienen juntos la base de datos, el almacenamiento, el adaptador y la configuración de middleware.

La plantilla de blog es una base útil para un sitio de contenido:

npm create astro@latest -- --template @emdash-cms/template-blog

Usa @emdash-cms/template-blog-cloudflare cuando el tema resultante apunte a Cloudflare Workers, D1 y R2.

Mantener la estructura de la plantilla

La plantilla de blog actual usa estas rutas 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/

Las plantillas starter, portfolio y marketing usan rutas distintas. Copia los archivos reales de la base elegida en lugar de asumir que todo tema tiene una ruta catch-all de páginas.

Apuntar al seed

Las plantillas actuales declaran la ruta del seed en package.json:

{
  "name": "@example/emdash-theme-publication",
  "private": true,
  "type": "module",
  "emdash": {
    "seed": "seed/seed.json"
  }
}

EmDash también descubre .emdash/seed.json y el fallback convencional seed/seed.json. Usa el campo del paquete al distribuir una plantilla para que el archivo previsto quede explícito.

Definir el modelo de contenido

Si partes de la plantilla de blog, edita su seed/seed.json existente en lugar de sustituirla por un modelo ajeno. El seed reducido siguiente conserva las colecciones y los datos estructurales usados en los ejemplos de esta guía:

{
  "$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": []
        }
      }
    ]
  }
}

La id de una entrada de contenido es un identificador local del seed usado por referencias. No está obligada a convertirse en el ID de base de datos de una entrada enrutable. Su slug pasa a ser el identificador de ruta expuesto como entry.id por la API de consulta.

Lee Archivos seed antes de añadir taxonomías, bylines, referencias de menú, medios, redirecciones, áreas de widgets, sections, localización o comportamiento ante conflictos.

Construir rutas renderizadas en el servidor

Las plantillas EmDash actuales usan output: "server". Las rutas consultan contenido en vivo en cada solicitud. No añadas getStaticPaths() a las rutas de contenido de un tema salvo que la plantilla use EmDash deliberadamente solo como fuente en tiempo de compilación.

El archivo siguiente ordena en la base de datos usando el campo almacenado 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 es un objeto de campo a dirección. Usa orderBy: { published_at: "desc" }, no sort, sortBy ni un callback de JavaScript.

La ruta siguiente resuelve y renderiza una entrada:

---
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>

Usa post.id en las URL de ruta y post.data.id donde un helper requiera el ID de contenido almacenado.

Consultar la navegación gestionada por el sitio

Los valores gestionados por el CMS deben obtenerse de las API correspondientes. Las plantillas actuales usan getSiteSettings(), getMenu() y <WidgetArea /> en sus 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>

El copy de diseño estático puede permanecer en archivos Astro. Los valores que se espera que editen los administradores deben representarse en ajustes, contenido, menús o widgets.

Renderizar campos de imagen

Los campos de imagen son valores de medios, no cadenas URL. Pasa el valor completo al componente Image para que el almacenamiento local y los proveedores de imagen se resuelvan de forma coherente:

---
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>

El componente usa el texto alternativo del propio campo salvo que lo sobrescribas. Reserva priority para una imagen que se espere above the fold; el resto permanece con carga diferida.

Ofrecer opciones de layout de página

El seed inicial anterior incluye un campo select para sitios donde los editores necesitan más de un layout de página. Al añadir la función a otro seed, usa valores estables que correspondan a componentes conocidos:

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

La ruta puede entonces elegir de un 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} />

El mapa explícito evita que un valor de campo almacenado se convierta en una ruta de módulo arbitraria.

Añadir búsqueda

Habilita search en cada colección que deba aparecer en resultados y marca los campos relevantes como searchable. Las plantillas actuales usan LiveSearch para una ruta de búsqueda lista:

---
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>

En un sitio Astro i18n, LiveSearch usa Astro.currentLocale. Pasa locale={null} solo cuando la página busque intencionalmente en todos los locales.

Seed de sections reutilizables

Las sections dan a los editores puntos de partida Portable Text reutilizables. Añádelas cuando el diseño incluya un patrón de contenido repetido, como una llamada a la acción:

{
  "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" }
          ]
        }
      ]
    }
  ]
}

El asistente de configuración permite omitir entradas seed, bylines y términos de taxonomía. Las sections y el resto del modelo estructural se aplican igualmente.

Añadir blocks de página estructurados

Usa un campo blocks cuando la página en sí sea una composición ordenada de secciones tipadas. Usa un block Portable Text personalizado cuando el objeto pertenezca dentro de un documento rich text y participe en el flujo de párrafos.

La plantilla marketing hace seed de cinco tipos de block retenidos y los permite en el campo content de Pages. Su wrapper asigna cada _type generado a un 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} />

La unión PageContentBlock generada mantiene el mapa de componentes alineado con cada tipo de block permitido y versión retenida. Cada valor de block en seed incluye _type, _version y un _key estable.

Usa un block Portable Text personalizado para un objeto que va entre párrafos, como un diagrama incrustado en un artículo. Asigna ese objeto mediante la opción components.type del componente PortableText. Un seed puede incluir el valor almacenado, pero un plugin nativo debe registrar un editor personalizado para crear y editar ese objeto Portable Text.

Usa un plugin nativo cuando el block necesite un editor personalizado reutilizable y componentes de renderizado empaquetados. Los plugins en sandbox no pueden incluir componentes de renderizado Astro en la compilación del sitio.

Probar la plantilla

  1. Andamia la plantilla en un directorio limpio con create-astro.

  2. Instala dependencias y ejecuta los comandos de compilación y comprobación de tipos del sitio.

  3. Empieza con una base de datos vacía y completa /_emdash/admin/setup.

  4. Prueba la configuración con contenido de ejemplo incluido y excluido.

  5. Abre cada ruta con contenido seed, contenido recién creado, sin imagen opcional y con una colección vacía.

  6. Edita ajustes del sitio, menús, asignaciones de taxonomía, Portable Text y blocks desde el admin. Para blocks, añade, reordena, duplica y elimina cada tipo permitido. Confirma que las rutas públicas usan los valores cambiados.

  7. Aplica la configuración específica de despliegue en un entorno desechable y verifica almacenamiento de medios, vistas previas y renderizado en servidor.

Publicar la plantilla

Un repositorio de GitHub puede usarse directamente con la sintaxis de plantilla github: de Astro:

npm create astro@latest -- --template github:example/emdash-theme-publication

Antes de publicar, elimina bases de datos y subidas locales, mantén secretos fuera del repositorio, verifica rutas de paquetes desde un checkout limpio y documenta qué destino de despliegue configura la plantilla.

Lista de comprobación

  • astro.config.mjs usa salida de servidor y los adaptadores de despliegue previstos.
  • src/live.config.ts registra emdashLoader().
  • package.json#emdash.seed apunta a un archivo existente.
  • Toda colección y campo consultados están declarados en el seed.
  • Los ejemplos de consulta usan orderBy y el identificador de entrada correcto.
  • Los valores del sitio gestionados por el CMS no están duplicados como constantes permanentes de la plantilla.
  • Una configuración limpia funciona con y sin contenido de ejemplo.
  • La compilación y la comprobación de tipos de la plantilla pasan para su destino soportado.