EmDash 主題是其他開發者可用 create-astro 腳手架建立的 Astro 專案。將專案作為完整網站建置與測試,並包含 seed,以建立其路由與元件所期望的內容模型。
從現有範本開始
選擇最接近目標網站與部署目標的現有範本。Node 與 Cloudflare 變體將資料庫、儲存、adapter 與 middleware 設定放在一起。
部落格範本適合作為內容網站的基礎:
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。目前範本在 layout 中使用 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使用伺服器輸出與預期的部署 adapter。 -
src/live.config.ts註冊emdashLoader()。 -
package.json#emdash.seed指向存在的檔案。 - 每個被查詢的集合與欄位均在 seed 中宣告。
- 查詢範例使用
orderBy與正確的項目識別碼。 - CMS 管理的網站值未作為永久範本常數重複。
- 乾淨設定在有無範例內容時均可運作。
- 範本建置與型別檢查在支援的目標上通過。