EmDash のブログテンプレートは、投稿、ページ、著者、カテゴリー、タグ、検索、コメント、ウィジェット、RSS フィードを備えた、動作する Astro サイトを提供します。このチュートリアルでは Cloudflare 版を作成し、投稿を 1 件公開して、その投稿がテンプレートのコード内をどう流れるかをたどります。
前提条件
始める前に、Node.js 22.12 以降と pnpm をインストールしてください。
Cloudflare アカウントが必要になるのは、サイトをデプロイするときだけです。ローカル開発中は、テンプレートがデータベースとファイルストレージのローカル版をお使いのコンピューター上で実行します。
ブログを構築する
次のコマンドは、Cloudflare のブログテンプレートから my-blog を作成し、pnpm で依存関係をインストールします。
npm create emdash@latest my-blog -- --template cloudflare:blog --pm pnpm --yes
スキャフォールダーは、EMDASH_ENCRYPTION_KEY を含むローカルの .env ファイルも作成します。生成される .gitignore は .env をバージョン管理から除外します。依存関係のインストールに失敗したとコマンドが報告した場合は、プロジェクトディレクトリに移動して pnpm install を実行してから続けてください。
ローカル開発サーバーを起動します。
cd my-blog
pnpm dev
ターミナルに表示されたローカル URL を開き、続いて /_emdash/admin を開きます。初回起動の場合は、セットアップ画面を完了してください。テンプレートのシードデータが、セットアップ中にコンテンツモデルとサンプルコンテンツを作成します。
コンテンツモデルを理解する
テンプレートは seed/seed.json で 2 つのコレクションを定義しています。
postsはsupportsを通じて下書き、リビジョン、検索、SEO を有効にし、コメントはcommentsEnabled: trueで個別に有効にします。pagesは下書き、リビジョン、検索をサポートします。
各投稿には次のカスタムフィールドがあります。
| フィールド | 用途 |
|---|---|
title | 必須の投稿タイトル |
featured_image | 投稿とともに表示される任意の画像 |
content | Portable Text の本文 |
excerpt | 投稿一覧やメタデータのフォールバックに使う短いテキスト |
EmDash は、安定したコンテンツ ID、スラッグ、ステータス、作成日時と更新日時、公開日時などのシステムフィールドを追加します。テンプレートは、投稿用の category と tag のタクソノミーに加え、1 人以上の著者をクレジットできる Byline も定義しています。
開発サーバーは、このスキーマから emdash-env.d.ts を生成します。その結果、getEmDashCollection("posts") が返すエントリの data プロパティは Post 型になります。
最初の投稿を公開する
-
管理画面のサイドバーで Posts を選び、続いて Add New を選びます。
-
タイトルを入力します。EmDash はタイトルからスラッグを提案します。公開 URL に別の値が必要な場合は編集してください。
-
抜粋を追加し、Content エディターで本文を書きます。
-
メディアライブラリからアイキャッチ画像を選ぶか、アップロードします。投稿内でのその画像の役割を説明する代替テキストを追加してください。
-
設定パネルで Byline、カテゴリー、関連するタグを割り当てます。
-
Save を選びます。エントリは下書きになり、エディターはそのエントリの固定 URL を開きます。
-
Preview を選んで投稿ページを確認します。下書きの準備ができたら、エディターに戻って Publish を選びます。
ローカルサイトで /posts/your-post-slug を開きます。投稿はホームページと投稿アーカイブにも表示されます。表示されない場合は、エディターに Draft や Scheduled ではなく Published と表示されていることを確認してください。
公開後は、編集内容は現在の投稿が公開されたまま新しい下書きに自動保存されます。修正した下書きで置き換える準備ができたら、Publish changes を選びます。プレビュー、スケジュール、リビジョン、編集ロックについては コンテンツ作成ガイド を参照してください。
コレクションクエリをたどる
ホームページと投稿アーカイブは、サーバーレンダリング中に getEmDashCollection() を呼び出します。テンプレートは、保存されている published_at フィールドでデータベース内の投稿を並べ替えます。
---
import { getEmDashCollection, getTermsForEntries } from "emdash";
const { entries: posts, cacheHint } = await getEmDashCollection("posts", {
orderBy: { published_at: "desc" },
});
if (Astro.cache?.enabled) Astro.cache.set(cacheHint);
const tagsByEntry = await getTermsForEntries(
"posts",
posts.map((post) => post.data.id),
"tag",
);
---
コレクションクエリは、デフォルトで公開済みのエントリを返します。並べ替えには、データベースのフィールド名である published_at を使います。返される publishedAt プロパティは、レンダリング用の JavaScript の Date です。
タクソノミーのヘルパーが post.data.id を受け取るのは、タクソノミーの割り当てが安定したコンテンツ ID に属するためです。一方、リンクには post.id を使います。これはコンテンツローダーが生成する、URL 向けのスラッグだからです。
<a href={`/posts/${post.id}`}>
<h2>{post.data.title}</h2>
{post.data.excerpt && <p>{post.data.excerpt}</p>}
</a>
現在のテンプレートは、投稿ごとに 1 回ずつクエリする代わりに、getTermsForEntries() でタグの検索をまとめて行います。Byline は、コレクションクエリによってすでに post.data.bylines に含まれています。
投稿クエリをたどる
動的な投稿ルートは、URL からスラッグを読み取って getEmDashEntry() を呼び出します。次の抜粋は、本質的なクエリとレンダリングの流れを示したものです。完全なテンプレートでは、SEO、Byline、コメント、関連投稿、ウィジェットも処理します。
---
import { decodeSlug, getEmDashEntry } from "emdash";
import { Image, PortableText } from "emdash/ui";
const slug = decodeSlug(Astro.params.slug);
if (!slug) return Astro.rewrite("/404");
const { entry: post, error, cacheHint } = await getEmDashEntry("posts", slug);
if (error) return new Response("Unable to load post", { status: 500 });
if (!post) return Astro.rewrite("/404");
if (Astro.cache?.enabled) Astro.cache.set(cacheHint);
---
<article>
{post.data.featured_image && <Image image={post.data.featured_image} priority />}
<h1>{post.data.title}</h1>
<PortableText value={post.data.content} />
</article>
Image は、編集者が選んだメディアの値を読み取り、レスポンシブな出力を生成します。PortableText は、保存されたブロックデータを、見出し、段落、リンク、画像、コードブロック、その他のサポートされているブロックタイプに変換します。
両方のブログテンプレートは、astro.config.mjs で output: "server" を設定しています。これらのクエリはリクエストがレンダリングされるときに実行されるため、公開済みコンテンツはビルド時に作成された静的ルートの一覧に依存しません。
カテゴリーとタグを使用する
テンプレートには、各カテゴリーとタグ用のアーカイブルートが含まれています。カテゴリーのルートは、まずターム slug を解決し、そのタクソノミーで投稿を絞り込みます。
---
import { decodeSlug, getEmDashCollection, getTerm } from "emdash";
const slug = decodeSlug(Astro.params.slug);
const category = slug
? await getTerm("category", slug, { includeCounts: false })
: null;
if (!category) return Astro.rewrite("/404");
const { entries: posts, error } = await getEmDashCollection("posts", {
where: { category: category.slug },
orderBy: { published_at: "desc" },
});
if (error) return new Response("Unable to load posts", { status: 500 });
---
tag ルートも同じパターンで、getTerm("tag", slug) と where: { tag: term.slug } を使います。タームとその割り当ては、編集者が管理画面で管理します。階層的なカテゴリー、フラットなタグ、カスタムタクソノミーについては タクソノミーのガイド を参照してください。
getEmDashEntry() は投稿に割り当てられたタームを含めるため、詳細ルートは追加のクエリなしでそれらをレンダリングできます。
---
const categories = post.data.terms?.category ?? [];
const tags = post.data.terms?.tag ?? [];
---
{categories.map((category) => (
<a href={`/category/${category.slug}`}>{category.label}</a>
))}
{tags.map((tag) => (
<a href={`/tag/${tag.slug}`}>{tag.label}</a>
))}
アーカイブのページネーションを追加する
テンプレートの投稿アーカイブは、公開済みのすべての投稿をレンダリングします。アーカイブが大きくなったら limit を追加し、/posts/page/2 のような番号付きルートにはオフセットページネーションを、Older posts リンクにはカーソルページネーションを使います。エントリの順序が予期せず変わらないよう、ページ間で orderBy: { published_at: "desc" } は変更しないでください。
ページネーションの例 では、両方の方法を示し、それぞれをどう選ぶかを説明しています。
RSS フィードを確認する
テンプレートはすでに /rss.xml を提供しています。そのエンドポイントは、getEmDashCollection() で最新の 20 件の投稿を読み取り、公開日をそれぞれ整形し、タイトルと抜粋を XML に挿入する前にエスケープします。サイトのタイトルとタグラインも EmDash の設定から読み取ります。
テスト用の投稿を公開したら、/rss.xml を開いてそのタイトルを検索してください。フィードで本番の絶対 URL を使う場合は、デプロイ前に Astro の site オプションを設定してください。ローカル開発中は、エンドポイントが現在のリクエストのオリジンにフォールバックします。
ここまでで、ブログには、編集ワークフロー、ランタイムの投稿ページ、タクソノミーのアーカイブ、メディアのレンダリング、フィードが揃いました。フィルターとページネーションについては コンテンツのクエリ へ、アセットの編集と使用状況の追跡については メディアライブラリ へ進んでください。AI アシスタントで投稿を下書きまたは編集するには、AI ツール に従ってください。