ブログを作成する

このページ

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投稿とともに表示されるオプションの画像
contentPortable Text 本文
excerpt投稿リストやメタデータフォールバックで使用される短いテキスト

EmDash は安定したコンテンツ ID、スラグ、ステータス、作成・更新時刻、公開時刻などのシステムフィールドを追加します。テンプレートは投稿用の category と tag タクソノミーも定義し、1人以上の著者をクレジットできる著者行も含みます。

開発サーバーはこのスキーマから emdash-env.d.ts を生成します。その結果、getEmDashCollection("posts") は data プロパティが Post として型付けされたエントリを返します。

最初の投稿を公開する

  1. 管理サイドバーで Posts を選択し、次に Add New を選択します。

  2. タイトルを入力します。EmDash はタイトルからスラグを提案します。パブリック URL に異なる値が必要な場合は編集してください。

  3. 抜粋を追加し、Content エディターで本文を書きます。

  4. メディアライブラリからフィーチャー画像を選択するかアップロードします。投稿での画像の目的を説明する代替テキストを追加します。

  5. 設定パネルで著者行、カテゴリー、関連タグを割り当てます。

  6. Save を選択します。エントリは下書きになり、エディターがその永続的なエントリ URL を開きます。

  7. 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() でタグルックアップをバッチ処理します。著者行はコレクションクエリによって post.data.bylines にすでに含まれています。

投稿クエリの追跡

動的投稿ルートは URL からスラグを読み取り、getEmDashEntry() を呼び出します。以下の抜粋は基本的なクエリとレンダリングパスを示しています。完全なテンプレートは SEO、著者行、コメント、関連投稿、ウィジェットも処理します:

---
import { decodeSlug, getEmDashEntry } from "emdash";
import { Image, PortableText } from "emdash/ui";

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("Unable to load post", { status: 500 });
if (!post) return Astro.redirect("/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" を設定しています。これらのクエリはリクエストがレンダリングされる時に実行されるため、公開コンテンツはビルド中に作成された静的ルートリストに依存しません。

カテゴリーとタグを使用する

テンプレートには各カテゴリーとタグのアーカイブルートが含まれています。カテゴリールートはまず用語スラグを解決し、次にそのタクソノミーで投稿をフィルタリングします:

---
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.redirect("/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 のような番号付きルートにはオフセットページネーションを、古い投稿リンクにはカーソルページネーションを使用します。ページ間で orderBy: { published_at: "desc" } を変更しないでください。エントリの順序が予期せず変わらないようにするためです。

ページネーションの例では両方のアプローチを示し、それぞれをいつ選択するかを説明しています。

RSS フィードを確認する

テンプレートはすでに /rss.xml を提供しています。そのエンドポイントは getEmDashCollection() で最新の20投稿を読み取り、各公開日をフォーマットし、タイトルと抜粋をエスケープしてから XML に挿入します。また、EmDash の設定からサイトタイトルとタグラインも読み取ります。

テスト投稿を公開した後、/rss.xml を開いてそのタイトルを検索してください。サイトがフィードで絶対本番 URL を使用する場合、デプロイ前に Astro の site オプションを設定してください。エンドポイントはローカル開発中は現在のリクエストオリジンにフォールバックします。

この時点でブログにはオーサリングワークフロー、ランタイム投稿ページ、タクソノミーアーカイブ、メディアレンダリング、フィードがあります。フィルターとページネーションについてはコンテンツのクエリ、アセット編集と使用状況追跡についてはメディアライブラリに進んでください。AI アシスタントで投稿を下書きまたは編集するには、AI ツールに従ってください。