EmDash 主题是其他开发者可用 create-astro 脚手架的 Astro 项目。将项目作为完整站点构建与测试,并包含 seed,以创建其路由与组件所期望的内容模型。
从现有模板开始
选择最接近目标站点与部署目标的现有模板。Node 与 Cloudflare 变体将数据库、存储、适配器与中间件配置放在一起。
博客模板适合作为内容站点的基础:
npm create astro@latest -- --template @emdash-cms/template-blog
若最终主题面向 Cloudflare Workers、D1 与 R2,请使用 @emdash-cms/template-blog-cloudflare。
保持模板结构
当前博客模板使用以下相关路径:
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/
starter、portfolio 与 marketing 模板使用不同路由。从所选基础复制实际文件,不要假设每个主题都有 catch-all 页面路由。
指向 seed
当前模板在 package.json 中声明 seed 路径:
{
"name": "@example/emdash-theme-publication",
"private": true,
"type": "module",
"emdash": {
"seed": "seed/seed.json"
}
}
EmDash 也会发现 .emdash/seed.json 与惯例回退路径 seed/seed.json。分发模板时使用包字段,使目标文件明确。
定义内容模型
从博客模板起步时,应编辑其现有 seed/seed.json,而不是替换为无关模型。下列精简 seed 保留本指南示例所用的集合与结构数据:
{
"$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": []
}
}
]
}
}
内容条目的 id 是供引用使用的 seed 本地标识符,不必成为可路由条目的数据库 ID。其 slug 成为查询 API 以 entry.id 暴露的路由标识符。
在添加分类、署名、菜单引用、媒体、重定向、小部件区域、section、本地化或冲突行为之前,请先阅读 Seed 文件。
构建服务端渲染路由
当前 EmDash 模板使用 output: "server"。路由在每次请求时查询实时内容。除非模板刻意仅将 EmDash 用作构建时数据源,否则不要为主题内容路由添加 getStaticPaths()。
下列归档页使用存储字段 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 是字段到方向的对象。应使用 orderBy: { published_at: "desc" },而不是 sort、sortBy 或 JavaScript 回调。
下列路由解析并渲染单篇文章:
---
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>
在路由 URL 中使用 post.id,在需要存储内容 ID 的辅助函数处使用 post.data.id。
查询站点管理的导航
CMS 管理的值应来自对应 API。当前模板在布局中使用 getSiteSettings()、getMenu() 与 <WidgetArea />:
---
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>
静态设计文案可保留在 Astro 文件中。管理员应能编辑的值必须在设置、内容、菜单或小部件中体现。
渲染图片字段
图片字段是媒体值,而非 URL 字符串。将完整值传给 Image 组件,以便本地存储与图片提供商一致解析:
---
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>
除非覆盖,组件会使用字段自身的 alt 文本。仅对预期出现在首屏的图片使用 priority;其余保持懒加载。
提供页面布局选项
上述 starter seed 包含 select 字段,供编辑者需要多种页面布局的站点使用。向其他 seed 添加该功能时,使用映射到已知组件的稳定值:
{
"slug": "template",
"label": "Page template",
"type": "select",
"defaultValue": "default",
"validation": {
"options": ["default", "full-width", "landing"]
}
}
路由随后可从显式组件映射中选择:
---
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} />
显式映射可防止存储的字段值变成任意模块路径。
添加搜索
在应出现在结果中的每个集合上启用 search,并将相关字段标记为 searchable。当前模板使用 LiveSearch 作为现成的搜索路由:
---
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>
在 Astro i18n 站点上,LiveSearch 使用 Astro.currentLocale。仅当页面有意搜索所有语言环境时传递 locale={null}。
Seed 可复用的 section
section 为编辑者提供可复用的 Portable Text 起点。当设计包含重复内容模式(如行动号召)时添加它们:
{
"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" }
]
}
]
}
]
}
设置向导允许用户省略已 seed 的条目、署名与分类术语。section 与其余结构模型仍会应用。
添加结构化页面 block
当页面本身是类型化区块的有序组合时,使用 blocks 字段。当对象属于富文本文档并参与段落流时,使用自定义 Portable Text block。
marketing 模板 seed 五种 retained block 类型,并允许在 Pages 的 content 字段中使用。其包装器将每个生成的 _type 映射到 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} />
生成的 PageContentBlock 联合类型使组件映射与每种允许的 block 类型及 retained 版本保持一致。每个 seed 的 block 值包含 _type、_version 与稳定的 _key。
对位于段落之间的对象(如文章中的嵌入图)使用自定义 Portable Text block,并通过 PortableText 组件的 components.type 选项映射。seed 可包含存储值,但原生插件必须注册自定义编辑器以创建和编辑该 Portable Text 对象。
当 block 需要可复用的自定义编辑器与打包的渲染组件时,使用原生插件。沙箱插件无法将 Astro 渲染组件打入站点构建。
测试模板
-
使用
create-astro将模板脚手架到干净目录。 -
安装依赖并运行站点的构建与类型检查命令。
-
从空数据库开始并完成
/_emdash/admin/setup。 -
分别测试包含与不包含示例内容的设置。
-
在 seed 内容、新创建内容、无可选图片与空集合等情况下打开每条路由。
-
在管理后台编辑站点设置、菜单、分类分配、Portable Text 与 block。对 block,添加、重排、复制并删除每种允许的类型。确认公开路由使用变更后的值。
-
在一次性环境中应用部署相关设置,并验证媒体存储、预览与服务端渲染。
发布模板
GitHub 仓库可直接配合 Astro 的 github: 模板语法使用:
npm create astro@latest -- --template github:example/emdash-theme-publication
发布前请删除本地数据库与上传文件、避免将密钥放入仓库、从干净 checkout 验证包路径,并文档化模板所配置的部署目标。
检查清单
-
astro.config.mjs使用服务端输出与预期的部署适配器。 -
src/live.config.ts注册emdashLoader()。 -
package.json#emdash.seed指向存在的文件。 - 每个被查询的集合与字段均在 seed 中声明。
- 查询示例使用
orderBy与正确的条目标识符。 - CMS 管理的站点值未作为永久模板常量重复。
- 干净设置在有无示例内容时均可工作。
- 模板构建与类型检查在支持的目标上通过。