Un tema de EmDash es un proyecto de Astro que otro desarrollador puede generar con create-astro. Construya y pruebe el proyecto como un sitio completo y luego incluya un seed que cree el modelo de contenido que esperan sus rutas y componentes.
Partir de una plantilla actual
Elija la plantilla existente más cercana al sitio previsto y al destino de despliegue. Las variantes de Node y de Cloudflare mantienen juntas su configuración de base de datos, almacenamiento, adaptador y middleware.
La plantilla de blog es una base útil para un sitio de contenido:
npm create astro@latest -- --template @emdash-cms/template-blog
Use @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. Copie archivos reales de la base elegida en lugar de suponer que todos los temas tienen una ruta de página comodín.
Apuntar al seed
Las plantillas actuales declaran la ruta de su 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 la alternativa convencional seed/seed.json. Use el campo del paquete cuando distribuya una plantilla para que el archivo previsto sea explícito.
Definir el modelo de contenido
Si parte de la plantilla de blog, edite su seed/seed.json existente en lugar de reemplazarlo por un modelo sin relación. El siguiente seed reducido conserva las collections y los datos estructurales que usan 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": []
}
}
]
}
}
El id de la entrada de contenido es un identificador local del seed que usan las referencias. No se fuerza a que sea el ID de base de datos de una entrada routable. Su slug se convierte en el identificador de ruta que la API de consultas expone como entry.id.
Lea Archivos seed antes de añadir taxonomías, bylines, referencias de menú, medios, redirecciones, áreas de widgets, sections, localización o comportamiento de conflictos.
Construir rutas renderizadas en el servidor
Las plantillas actuales de EmDash usan output: "server". Las rutas consultan contenido en vivo en cada solicitud. No añada getStaticPaths() a las rutas de contenido de un tema, a menos que la plantilla use EmDash deliberadamente solo como fuente en tiempo de compilación.
El siguiente archivo 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. Use orderBy: { published_at: "desc" }, no sort, sortBy ni una función de callback de JavaScript.
La siguiente ruta resuelve y renderiza una publicación:
---
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 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 provenir 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 texto estático del diseño puede permanecer en los archivos de 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 de URL. Pase el valor completo al componente Image para que el almacenamiento local y los proveedores de imágenes 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 propio del campo, salvo que lo sustituya. Reserve priority para una imagen que se espera que quede en la parte visible sin desplazarse; las demás imágenes siguen cargándose de forma diferida.
Ofrecer opciones de layout de página
El seed inicial anterior incluye un campo select para sitios en los que los editores necesitan más de un layout de página. Al añadir la función a otro seed, use valores estables que se correspondan con componentes conocidos:
{
"slug": "template",
"label": "Page template",
"type": "select",
"defaultValue": "default",
"validation": {
"options": ["default", "full-width", "landing"]
}
}
La ruta puede entonces elegir a partir 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.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} />
El mapa explícito evita que un valor de campo almacenado se convierta en una ruta de módulo arbitraria.
Añadir búsqueda
Active search en cada collection que deba aparecer en los resultados y marque los campos relevantes como searchable. Las plantillas actuales usan LiveSearch para obtener una ruta de búsqueda ya preparada:
---
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 de Astro con i18n, LiveSearch usa Astro.currentLocale. Pase locale={null} solo cuando la página deba buscar deliberadamente en todos los locales.
Seed de sections reutilizables
Las sections ofrecen a los editores puntos de partida reutilizables de Portable Text. Añádalas 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 al usuario omitir las entradas, los bylines y los términos de taxonomía sembrados. Las sections y el resto del modelo estructural se aplican de todos modos.
Añadir blocks de página estructurados
Use un campo blocks cuando la página en sí sea una composición ordenada de sections tipadas. Use un block personalizado de Portable Text cuando el objeto pertenezca al interior de un documento de texto enriquecido y participe en su flujo de párrafos.
La plantilla marketing siembra cinco tipos de block conservados y los permite en el campo content de Pages. Su contenedor asigna cada _type generado a un componente de 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 cada versión conservada. Cada valor de block sembrado incluye _type, _version y un _key estable.
Use un block personalizado de Portable Text para un objeto que pertenezca entre párrafos, como un diagrama incrustado en un artículo. Asigne 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 de Portable Text.
Use 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 de Astro en la compilación de un sitio.
Probar la plantilla
-
Genere la plantilla en un directorio limpio con
create-astro. -
Instale las dependencias y ejecute los comandos de compilación y de comprobación de tipos del sitio.
-
Empiece con una base de datos vacía y complete
/_emdash/admin/setup. -
Pruebe la configuración con el contenido de ejemplo incluido y excluido.
-
Abra cada ruta con contenido sembrado, contenido recién creado, sin imagen opcional y con una collection vacía.
-
Edite los ajustes del sitio, los menús, las asignaciones de taxonomía, Portable Text y los blocks desde la administración. Para los blocks, añada, reordene, duplique y elimine cada tipo permitido. Confirme que las rutas públicas usan los valores modificados.
-
Aplique la configuración específica del despliegue en un entorno desechable y verifique el almacenamiento de medios, las vistas previas y el renderizado en el 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, elimine las bases de datos y las cargas locales, mantenga los secretos fuera del repositorio, verifique las rutas del paquete desde un checkout limpio y documente qué destino de despliegue configura la plantilla.
Lista de comprobación
-
astro.config.mjsusa salida de servidor y los adaptadores de despliegue previstos. -
src/live.config.tsregistraemdashLoader(). -
package.json#emdash.seedapunta a un archivo existente. - Cada collection y campo consultados están declarados por el seed.
- Los ejemplos de consulta usan
orderByy 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 compatible.