テーマの作成
EmDashテーマは、完全なAstroサイト(ページ、レイアウト、コンポーネント、スタイル)であり、コンテンツモデルをブートストラップするためのシードファイルも含まれています。デザインを他の人と共有したり、あなたの代理店でのサイト作成を標準化するために構築してください。
- テーマは動作するAstroプロジェクトです。 テーマAPIや抽象化レイヤーはありません。サイトを構築し、テンプレートとして出荷します。シードファイルは、初回実行時にEmDashが作成するコレクション、フィールド、メニュー、リダイレクト、タクソノミーを指示するだけです。
- EmDashはWordPressよりもコンテンツモデルに対する制御を強化します。 テーマはこれを活用します。シードファイルは、各コレクションが必要とするフィールドを正確に宣言します。標準のpostsおよびpagesコレクションを基盤とし、完全に新しいコンテンツタイプを発明するのではなく、デザインの要件に応じてフィールドやタクソノミーを追加してください。
- テーマのコンテンツページはサーバーサイドレンダリング(SSR)である必要があります。 テーマでは、コンテンツは管理UIを通じて実行時に変更されるため、EmDashコンテンツを表示するページは事前レンダリングされてはいけません。テーマのコンテンツルートでは
getStaticPaths()を使用しないでください。(EmDashをビルド時のデータソースとして使用する静的サイトビルドはgetStaticPathsを使用できますが、テーマは常にSSRです。) - ハードコードされたコンテンツはありません。 サイトタイトル、タグライン、ナビゲーション、その他の動的コンテンツは、テンプレート文字列ではなく、API呼び出しを介してCMSから取得されます。
プロジェクト構造
Section titled “プロジェクト構造”この構造でテーマを作成してください:
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の設定
Section titled “package.jsonの設定”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にあるスタンドアロンコンテンツです。
これが推奨される出発点です。テーマの必要に応じて、さらにコレクション、タクソノミー、フィールドを追加してください。
シードファイル
Section titled “シードファイル”シードファイルは、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を持ちます。ページはスタンドアロンコンテンツであるため、これらは必要ありません。テーマの要件に応じて、いずれかのコレクションにフィールドを追加してください。
セクション、ウィジェットエリア、メディア参照を含む完全な仕様については、シードファイル形式を参照してください。
ページの構築
Section titled “ページの構築”EmDashコンテンツを表示するすべてのページはサーバーサイドレンダリングされます。Astro.paramsを使用してURLからスラッグを取得し、リクエスト時にコンテンツをクエリしてください。
ホームページ
Section titled “ホームページ”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にヒットします。
カテゴリーアーカイブ
Section titled “カテゴリーアーカイブ”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>メニューの使用
Section titled “メニューの使用”レイアウト内で管理者定義のメニューをクエリします。ナビゲーションリンクをハードコードしないでください:
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>ページテンプレート
Section titled “ページテンプレート”テーマでは、複数のページレイアウト(デフォルトレイアウト、全幅レイアウト、ランディングページレイアウトなど)が必要になることがよくあります。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 titled “セクションの追加”セクションは、編集者が /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から削除できません。
サンプルコンテンツの追加
Section titled “サンプルコンテンツの追加”テーマのデザインを実演するために、シードファイルにサンプルコンテンツを含めます:
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"] } } ] }}メディアの含め方
Section titled “メディアの含め方”サンプルコンテンツ内の画像は $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(コンテンツタイプ > 編集 > 機能 > 検索)でコレクションごとに有効にする必要があります。
テーマのテスト
Section titled “テーマのテスト”-
テーマからテストプロジェクトを作成します:
Terminal window npm create astro@latest -- --template ./path/to/my-theme -
依存関係をインストールし、開発サーバーを起動します:
Terminal window cd test-sitenpm installnpm run dev -
`http://localhost:4321/_emdash/admin“ でセットアップウィザードを完了します
-
コレクション、メニュー、リダイレクト、コンテンツが正しく作成されたことを確認します
-
すべてのページテンプレートが正しくレンダリングされることをテストします
-
管理パネルを通じて新しいコンテンツを作成し、すべてのフィールドが機能することを確認します
テーマの公開
Section titled “テーマの公開”配布のためにnpmに公開します:
npm publish --access publicユーザーはその後、テーマをインストールできます:
npm create astro@latest -- --template @your-org/emdash-theme-blogGitHubでホストされるテーマの場合:
npm create astro@latest -- --template github:your-org/emdash-theme-blogカスタムPortable Textブロック
Section titled “カスタムPortable Textブロック”テーマは、特殊なコンテンツ用にカスタム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} />ナビゲーション用のアンカーID
Section titled “ナビゲーション用のアンカーID”リンク可能にするブロックに _key を追加します:
{ "_type": "marketing.features", "_key": "features", "headline": "Features"}次に、コンポーネント内でアンカーとして使用します:
<section id={value._key}> <!-- content --></section>これにより、/#features のようなナビゲーションリンクが可能になります。
テーマチェックリスト
Section titled “テーマチェックリスト”公開前に、テーマに以下が含まれていることを確認してください:
-
emdashフィールド(ラベル、説明、シードパス)を持つpackage.json - 有効なスキーマを持つ
.emdash/seed.json - ページで参照されるすべてのコレクションがシードに存在する
- レイアウトで使用されるメニューがシードで定義されている
- サンプルコンテンツがテーマのデザインを実演している
- データベースとストレージ設定を持つ
astro.config.mjs - EmDashローダーを持つ
src/live.config.ts - コンテンツページに
getStaticPaths()がない - サイトタイトル、タグライン、ナビゲーションがハードコードされていない
- 画像フィールドが文字列ではなくオブジェクト(
image.src)としてアクセスされる - セットアップ手順を含むREADME
- 非標準のPortable Textタイプ用のカスタムブロックコンポーネント
次のステップ
Section titled “次のステップ”- シードファイル形式 — シードファイルの完全なリファレンス
- テーマ概要 — EmDashでのテーマの仕組み
- WordPressテーマの移植 — 既存のWordPressテーマの変換