コンテンツにスキップ

テーマの作成

EmDashテーマは、完全なAstroサイト(ページ、レイアウト、コンポーネント、スタイル)であり、コンテンツモデルをブートストラップするためのシードファイルも含まれています。デザインを他の人と共有したり、あなたの代理店でのサイト作成を標準化するために構築してください。

  • テーマは動作するAstroプロジェクトです。 テーマAPIや抽象化レイヤーはありません。サイトを構築し、テンプレートとして出荷します。シードファイルは、初回実行時にEmDashが作成するコレクション、フィールド、メニュー、リダイレクト、タクソノミーを指示するだけです。
  • EmDashはWordPressよりもコンテンツモデルに対する制御を強化します。 テーマはこれを活用します。シードファイルは、各コレクションが必要とするフィールドを正確に宣言します。標準のpostsおよびpagesコレクションを基盤とし、完全に新しいコンテンツタイプを発明するのではなく、デザインの要件に応じてフィールドやタクソノミーを追加してください。
  • テーマのコンテンツページはサーバーサイドレンダリング(SSR)である必要があります。 テーマでは、コンテンツは管理UIを通じて実行時に変更されるため、EmDashコンテンツを表示するページは事前レンダリングされてはいけません。テーマのコンテンツルートではgetStaticPaths()を使用しないでください。(EmDashをビルド時のデータソースとして使用する静的サイトビルドはgetStaticPathsを使用できますが、テーマは常にSSRです。)
  • ハードコードされたコンテンツはありません。 サイトタイトル、タグライン、ナビゲーション、その他の動的コンテンツは、テンプレート文字列ではなく、API呼び出しを介してCMSから取得されます。

この構造でテーマを作成してください:

my-emdash-theme/
├── package.json # Theme metadata
├── astro.config.mjs # Astro + EmDash configuration
├── src/
│ ├── live.config.ts # Live Collections setup
│ ├── pages/
│ │ ├── index.astro # Homepage
│ │ ├── [...slug].astro # Pages (catch-all)
│ │ ├── posts/
│ │ │ ├── index.astro # Post archive
│ │ │ └── [slug].astro # Single post
│ │ ├── categories/
│ │ │ └── [slug].astro # Category archive
│ │ ├── tags/
│ │ │ └── [slug].astro # Tag archive
│ │ ├── search.astro # Search page
│ │ └── 404.astro # Not found
│ ├── layouts/
│ │ └── Base.astro # Base layout
│ └── components/ # Your components
├── .emdash/
│ ├── seed.json # Schema and sample content
│ └── uploads/ # Optional local media files
└── public/ # Static assets

ページはキャッチオールルート([...slug].astro)としてルートに配置されるため、スラッグaboutのページは/aboutでレンダリングされます。投稿、カテゴリー、タグはそれぞれ専用のディレクトリを持ちます。.emdash/ディレクトリには、シードファイルとサンプルコンテンツで使用されるローカルメディアファイルが含まれます。

package.jsonにemdashフィールドを追加してください:

json title="package.json"
{
"name": "@your-org/emdash-theme-blog",
"version": "1.0.0",
"description": "A minimal blog theme for EmDash",
"keywords": ["astro-template", "emdash", "blog"],
"emdash": {
"label": "Minimal Blog",
"description": "A clean, minimal blog with posts, pages, and categories",
"seed": ".emdash/seed.json",
"preview": "https://your-theme-demo.pages.dev"
}
}
フィールド説明
emdash.labelテーマピッカーに表示される表示名
emdash.descriptionテーマの簡単な説明
emdash.seedシードファイルへのパス
emdash.previewライブデモへのURL(オプション)

デフォルトのコンテンツモデル

Section titled “デフォルトのコンテンツモデル”

ほとんどのテーマには、postsとpagesの2つのコレクションタイプが必要です。投稿は、フィードやアーカイブに表示される抜粋とアイキャッチ画像を持つタイムスタンプ付きエントリーです。ページは、トップレベルのURLにあるスタンドアロンコンテンツです。

これが推奨される出発点です。テーマの必要に応じて、さらにコレクション、タクソノミー、フィールドを追加してください。

シードファイルは、EmDashに初回実行時に何を作成するかを指示します。.emdash/seed.jsonを作成してください:

json title=".emdash/seed.json"
{
"$schema": "https://emdashcms.com/seed.schema.json",
"version": "1",
"meta": {
"name": "ミニマルブログ",
"description": "投稿と固定ページを備えたシンプルなブログ",
"author": "あなたの名前"
},
"settings": {
"title": "私のブログ",
"tagline": "考えとアイデア",
"postsPerPage": 10
},
"collections": [
{
"slug": "posts",
"label": "Posts",
"labelSingular": "Post",
"supports": ["drafts", "revisions"],
"fields": [
{ "slug": "title", "label": "Title", "type": "string", "required": true },
{ "slug": "content", "label": "Content", "type": "portableText" },
{ "slug": "excerpt", "label": "Excerpt", "type": "text" },
{ "slug": "featured_image", "label": "Featured Image", "type": "image" }
]
},
{
"slug": "pages",
"label": "Pages",
"labelSingular": "Page",
"supports": ["drafts", "revisions"],
"fields": [
{ "slug": "title", "label": "Title", "type": "string", "required": true },
{ "slug": "content", "label": "Content", "type": "portableText" }
]
}
],
"taxonomies": [
{
"name": "category",
"label": "カテゴリー",
"labelSingular": "カテゴリー",
"hierarchical": true,
"collections": ["posts"],
"terms": [
{ "slug": "news", "label": "ニュース" },
{ "slug": "tutorials", "label": "チュートリアル" }
]
}
],
"menus": [
{
"name": "primary",
"label": "メインナビゲーション",
"items": [
{ "type": "custom", "label": "ホーム", "url": "/" },
{ "type": "custom", "label": "Blog", "url": "/posts" }
]
}
],
"redirects": [
{ "source": "/category/news", "destination": "/categories/news" },
{ "source": "/old-about", "destination": "/about" }
]
}

投稿はリストやフィードに表示されるため、excerptとfeatured_imageを持ちます。ページはスタンドアロンコンテンツであるため、これらは必要ありません。テーマの要件に応じて、いずれかのコレクションにフィールドを追加してください。

セクション、ウィジェットエリア、メディア参照を含む完全な仕様については、シードファイル形式を参照してください。

EmDashコンテンツを表示するすべてのページはサーバーサイドレンダリングされます。Astro.paramsを使用してURLからスラッグを取得し、リクエスト時にコンテンツをクエリしてください。

astro title="src/pages/index.astro"
---
import { getEmDashCollection, getSiteSettings } from "emdash";
import Base from "../../layouts/Base.astro";
const settings = await getSiteSettings();
const { entries: posts } = await getEmDashCollection("posts", {
where: { status: "published" },
orderBy: { publishedAt: "desc" },
limit: settings.postsPerPage ?? 10,
});
---
<Base title="ホーム">
<h1>最新の投稿</h1>
{posts.map((post) => (
<article>
<h2><a href={`/posts/${post.slug}`}>{post.data.title}</a>
</h2>
<p>{post.data.excerpt}</p>
</article>
))}
</Base>
astro title="src/pages/posts/[slug].astro"
---
import { getEmDashEntry, getEntryTerms } from "emdash";
import { PortableText } from "emdash/ui";
import Base from "../../../layouts/Base.astro";
const { slug } = Astro.params;
const { entry: post } = await getEmDashEntry("posts", slug!);
if (!post) {
return Astro.redirect("/404");
}
const categories = await getEntryTerms("posts", post.id, "categories");
---
<Base title={post.data.title}>
<article>
<h1>{post.data.title}</h1>
<PortableText value={post.data.content} />
<div class="post-meta">
{categories.map((cat) => (
<a href={`/categories/${cat.slug}`}>{cat.label}</a>
))}
</div>
</article>
</Base>

ページはルートにキャッチオールルートを使用するため、スラッグがトップレベルのURLに直接マッピングされます。スラッグaboutのページは/aboutでレンダリングされます:

astro title="src/pages/[...slug].astro"
---
import { getEmDashEntry } from "emdash";
import { PortableText } from "emdash/ui";
import Base from "../../layouts/Base.astro";
const { slug } = Astro.params;
const { entry: page } = await getEmDashEntry("pages", slug!);
if (!page) {
return Astro.redirect("/404");
}
---
<Base title={page.data.title}>
<article>
<h1>{page.data.title}</h1>
<PortableText value={page.data.content} />
</article>
</Base>

これはキャッチオールルートであるため、より具体的なルートがないURLにのみマッチします。/posts/hello-worldはこのファイルではなく、posts/[slug].astroにヒットします。

astro title="src/pages/categories/[slug].astro"
---
import { getTerm, getEntriesByTerm } from "emdash";
import Base from "../../../layouts/Base.astro";
const { slug } = Astro.params;
const category = await getTerm("categories", slug!);
const posts = await getEntriesByTerm("posts", "categories", slug!);
if (!category) {
return Astro.redirect("/404");
}
---
<Base title={category.label}>
<h1>{category.label}</h1>
{posts.map((post) => (
<article>
<h2><a href={`/posts/${post.slug}`}>{post.data.title}</a>
</h2>
</article>
))}
</Base>

画像フィールドは文字列ではなく、srcとaltプロパティを持つオブジェクトです。最適化された画像レンダリングには、emdash/uiのImageコンポーネントを使用してください:

astro title="src/components/PostCard.astro"
---
import { Image } from "emdash/ui";
const { post } = Astro.props;
---
<article>
{post.data.featured_image?.src && (
<Image
image={post.data.featured_image}
alt={post.data.featured_image.alt || post.data.title}
width={800}
height={450}
/>
)}
<h2><a href={`/posts/${post.slug}`}>{post.data.title}</a>
</h2>
<p>{post.data.excerpt}</p>
</article>

レイアウト内で管理者定義のメニューをクエリします。ナビゲーションリンクをハードコードしないでください:

astro title="src/layouts/Base.astro"
---
import { getMenu, getSiteSettings } from "emdash";
const settings = await getSiteSettings();
const primaryMenu = await getMenu("primary");
---
<html>
<head>
<title>{Astro.props.title} | {settings.title}</title>
</head>
<body>
<header>
{settings.logo ? (
<img src={settings.logo.url} alt={settings.title} />
) : (
<span>{settings.title}</span>
)}
<nav>
{primaryMenu?.items.map((item) => (
<a href={item.url}>{item.label}</a>
))}
</nav>
</header>
<main>
<slot />
</main>
</body>
</html>

テーマでは、複数のページレイアウト(デフォルトレイアウト、全幅レイアウト、ランディングページレイアウトなど)が必要になることがよくあります。EmDashでは、ページコレクションに template 選択フィールドを追加し、それをキャッチオールルートのレイアウトコンポーネントにマッピングします。

シードファイルのページコレクションにフィールドを追加します:

{
"slug": "template",
"label": "Page Template",
"type": "string",
"widget": "select",
"options": {
"choices": [
{ "value": "default", "label": "Default" },
{ "value": "full-width", "label": "Full Width" },
{ "value": "landing", "label": "Landing Page" }
]
},
"defaultValue": "default"
}

次に、キャッチオールルートで値をレイアウトコンポーネントにマッピングします:

astro title="src/pages/[...slug].astro"
---
import { getEmDashEntry } from "emdash";
import PageDefault from "../../layouts/PageDefault.astro";
import PageFullWidth from "../../layouts/PageFullWidth.astro";
import PageLanding from "../../layouts/PageLanding.astro";
const { slug } = Astro.params;
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={page} />

編集者は、管理UIでページを編集する際に、ドロップダウンからテンプレートを選択します。

セクションは、編集者が /section スラッシュコマンドを使用して任意のPortable Textフィールドに挿入できる再利用可能なコンテンツブロックです。テーマに共通のコンテンツパターン(ヒーローバナー、CTA、機能グリッドなど)がある場合は、シードファイルでセクションとして定義します:

json title=".emdash/seed.json"
{
"sections": [
{
"slug": "hero-centered",
"title": "Centered Hero",
"description": "Full-width hero with centered heading and CTA",
"keywords": ["hero", "banner", "header", "landing"],
"content": [
{
"_type": "block",
"style": "h1",
"children": [{ "_type": "span", "text": "私たちのサイトへようこそ" }]
},
{
"_type": "block",
"children": [
{ "_type": "span", "text": "魅力的なタグラインをここに入れます。" }
]
}
]
},
{
"slug": "newsletter-cta",
"title": "Newsletter Signup",
"keywords": ["newsletter", "subscribe", "email"],
"content": [
{
"_type": "block",
"style": "h3",
"children": [{ "_type": "span", "text": "Subscribe to our newsletter" }]
},
{
"_type": "block",
"children": [
{
"_type": "span",
"text": "Get the latest updates delivered to your inbox."
}
]
}
]
}
]
}

シードファイルから作成されたセクションには source: "theme" とマークされます。編集者は独自のセクション(source: "user" とマーク)を作成することもできますが、テーマ提供のセクションは管理UIから削除できません。

テーマのデザインを実演するために、シードファイルにサンプルコンテンツを含めます:

json title=".emdash/seed.json"
{
"content": {
"posts": [
{
"id": "hello-world",
"slug": "hello-world",
"status": "published",
"data": {
"title": "はじめまして",
"content": [
{
"_type": "block",
"style": "normal",
"children": [{ "_type": "span", "text": "新しいブログへようこそ!" }]
}
],
"excerpt": "EmDash での最初の投稿です。"
},
"taxonomies": {
"category": ["news"]
}
}
]
}
}

サンプルコンテンツ内の画像は $media 構文を使用して参照します。

リモート画像の場合:

{
"data": {
"featured_image": {
"$media": {
"url": "https://images.unsplash.com/photo-xxx",
"alt": "A descriptive alt text",
"filename": "hero.jpg"
}
}
}
}

ローカル画像の場合、ファイルを .emdash/uploads/ に配置し、参照します:

{
"data": {
"featured_image": {
"$media": {
"file": "hero.jpg",
"alt": "A descriptive alt text"
}
}
}
}

シーディング中に、メディアファイルはダウンロード(またはローカルで読み取り)され、ストレージにアップロードされます。

テーマに検索ページが含まれる場合は、インスタント結果のために LiveSearch コンポーネントを使用します:

astro title="src/pages/search.astro"
---
import LiveSearch from "emdash/ui/search";
import Base from "../../layouts/Base.astro";
---
<Base title="Search">
<h1>検索</h1>
<LiveSearch
placeholder="投稿とページを検索..."
collections={["posts", "pages"]}
/>
</Base>

LiveSearch は、デバウンスされたインスタント検索を提供し、プレフィックスマッチング、ポーターステミング、ハイライトされた結果スニペットを備えています。検索は、管理UI(コンテンツタイプ > 編集 > 機能 > 検索)でコレクションごとに有効にする必要があります。

  1. テーマからテストプロジェクトを作成します:

    Terminal window
    npm create astro@latest -- --template ./path/to/my-theme
  2. 依存関係をインストールし、開発サーバーを起動します:

    Terminal window
    cd test-site
    npm install
    npm run dev
  3. `http://localhost:4321/_emdash/admin“ でセットアップウィザードを完了します

  4. コレクション、メニュー、リダイレクト、コンテンツが正しく作成されたことを確認します

  5. すべてのページテンプレートが正しくレンダリングされることをテストします

  6. 管理パネルを通じて新しいコンテンツを作成し、すべてのフィールドが機能することを確認します

配布のためにnpmに公開します:

Terminal window
npm publish --access public

ユーザーはその後、テーマをインストールできます:

Terminal window
npm create astro@latest -- --template @your-org/emdash-theme-blog

GitHubでホストされるテーマの場合:

Terminal window
npm create astro@latest -- --template github:your-org/emdash-theme-blog

テーマは、特殊なコンテンツ用にカスタムPortable Textブロックタイプを定義できます。これは、マーケティングページ、ランディングページ、または標準的なリッチテキストを超えた構造化コンポーネントを必要とするコンテンツに役立ちます。

シードコンテンツでのカスタムブロックの定義

Section titled “シードコンテンツでのカスタムブロックの定義”

シードファイルのPortable Textコンテンツで名前空間付きの _type を使用します:

json title=".emdash/seed.json"
{
"content": {
"pages": [
{
"id": "home",
"slug": "home",
"status": "published",
"data": {
"title": "ホーム",
"content": [
{
"_type": "marketing.hero",
"headline": "Build something amazing",
"subheadline": "The all-in-one platform for modern teams.",
"primaryCta": { "label": "Get Started", "url": "/signup" }
},
{
"_type": "marketing.features",
"_key": "features",
"headline": "Everything you need",
"features": [
{
"icon": "zap",
"title": "Lightning fast",
"description": "Built for speed."
}
]
}
]
}
}
]
}
}

ブロックコンポーネントの作成

Section titled “ブロックコンポーネントの作成”

各カスタムブロックタイプ用にAstroコンポーネントを作成します:

astro title="src/components/blocks/Hero.astro"
---
interface Props {
value: {
headline: string;
subheadline?: string;
primaryCta?: { label: string; url: string };
};
}
const { value } = Astro.props;
---
<section class="hero">
<h1>{value.headline}</h1>
{value.subheadline && <p>{value.subheadline}</p>
}
{value.primaryCta && (
<a href={value.primaryCta.url} class="btn">
{value.primaryCta.label}
</a>
)}
</section>

カスタムブロックのレンダリング

Section titled “カスタムブロックのレンダリング”

カスタムブロックコンポーネントを PortableText コンポーネントに渡します:

astro title="src/components/MarketingBlocks.astro"
---
import { PortableText } from "emdash/ui";
import Hero from "../../themes/blocks/Hero.astro";
import Features from "../../themes/blocks/Features.astro";
interface Props {
value: unknown[];
}
const { value } = Astro.props;
const marketingTypes = {
"marketing.hero": Hero,
"marketing.features": Features,
};
---
<PortableText value={value} components={{ types: marketingTypes }} />

次に、ページ内で使用します:

astro title="src/pages/index.astro"
---
import { getEmDashEntry } from "emdash";
import MarketingBlocks from "../../components/MarketingBlocks.astro";
const { entry: page } = await getEmDashEntry("pages", "home");
---
<MarketingBlocks value={page.data.content} />

リンク可能にするブロックに _key を追加します:

{
"_type": "marketing.features",
"_key": "features",
"headline": "Features"
}

次に、コンポーネント内でアンカーとして使用します:

<section id={value._key}>
<!-- content -->
</section>

これにより、/#features のようなナビゲーションリンクが可能になります。

公開前に、テーマに以下が含まれていることを確認してください:

  • emdash フィールド(ラベル、説明、シードパス)を持つ package.json
  • 有効なスキーマを持つ .emdash/seed.json
  • ページで参照されるすべてのコレクションがシードに存在する
  • レイアウトで使用されるメニューがシードで定義されている
  • サンプルコンテンツがテーマのデザインを実演している
  • データベースとストレージ設定を持つ astro.config.mjs
  • EmDashローダーを持つ src/live.config.ts
  • コンテンツページに getStaticPaths() がない
  • サイトタイトル、タグライン、ナビゲーションがハードコードされていない
  • 画像フィールドが文字列ではなくオブジェクト(image.src)としてアクセスされる
  • セットアップ手順を含むREADME
  • 非標準のPortable Textタイプ用のカスタムブロックコンポーネント