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 として公開されるルート識別子になります。
タクソノミー、byline、メニュー参照、メディア、リダイレクト、ウィジェットエリア、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 はフィールドから方向へのオブジェクトです。sort、sortBy、JavaScript コールバックではなく orderBy: { published_at: "desc" } を使います。
次のルートは 1 件の投稿を解決して表示します:
---
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 テキストを使います。above the fold が想定される画像にだけ priority を使い、他は lazy-loaded のままにします。
ページレイアウトの選択肢を提供する
上記の 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} を渡します。
再利用可能な section を seed する
section は編集者に再利用可能な Portable Text の起点を与えます。CTA のような繰り返しコンテンツパターンがあるデザインでは追加します:
{
"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 済みエントリ、byline、タクソノミー用語を省略できます。section と構造モデルの残りは引き続き適用されます。
構造化ページ block を追加する
ページ自体が型付きセクションの順序付き構成であるときは blocks フィールド を使います。リッチテキスト文書の中に属し段落フローに参加するオブジェクトには、カスタム Portable Text block を使います。
marketing テンプレートは 5 つの retained block 型を seed し、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
公開前にローカル DB とアップロードを削除し、シークレットをリポジトリに含めず、クリーンな checkout からパッケージパスを検証し、テンプレートが設定するデプロイ先を文書化してください。
チェックリスト
-
astro.config.mjsがサーバー出力と意図したデプロイアダプターを使っている。 -
src/live.config.tsがemdashLoader()を登録している。 -
package.json#emdash.seedが存在するファイルを指している。 - クエリするすべてのコレクションとフィールドが seed で宣言されている。
- クエリ例が
orderByと正しいエントリ識別子を使っている。 - CMS 管理のサイト値がテンプレート定数として重複していない。
- クリーンなセットアップがサンプルコンテンツあり・なしの両方で動作する。
- テンプレートのビルドと型チェックがサポート対象で成功する。