このサイトは非公式の日本語訳です。Cloudflare・EmDashプロジェクトが運営するサイトではありません。

このページで分かること

  • 公式テンプレートが使う関数の一覧と、コンテンツを取得する関数(getEmDashCollection()getEmDashEntry() など)の引数と戻り値
  • 取得したエントリーの形、URL用のヘルパー、プレビュー、Portable Textの変換、サイト設定、SEOの関数
  • コメント、メニュー、バイライン、タクソノミー、ウィジェットエリア、セクション、検索の関数と、エラーの扱い
難易度
上級
読む時間
8分
このページの目次

このページでは、AstroのページやレイアウトやコンポーネントがEmDashのサイトを読み込んで表示するために使う、公開APIを説明します。emdash パッケージのルートからエクスポートされるものをすべて一覧にするものではありません。データベースのリポジトリ、APIのハンドラー、マイグレーションのユーティリティ、プラグイン作成用のAPI、そのほかのサーバー内部の仕組みは、別のリファレンスがあるか、フレームワークとの統合のためのコードが使うことを想定しています。

このリファレンスのサイトテンプレートAPIの契約には、EmDashが保守しているサイトテンプレートが emdash からインポートしているすべての関数を載せています。MediaValue のような型だけのインポートは、関係するデータモデルのドキュメントで扱います。サイトの作成者に役立つ場合は関連する関数も同じセクションで説明していますが、関係のないルートのエクスポートは扱いません。

サイトテンプレートAPIの契約

保守されているテンプレートは、次のランタイムのヘルパーを呼び出します。

関数 用途
decodeSlug 取得の前に、動的ルートのパラメーターをデコードします
getEmDashCollection コレクションのエントリーを読み込み、絞り込みます
getEmDashEntry IDまたはスラッグで1件のエントリーを読み込みます
getMenu ナビゲーションのメニューと、解決済みのリンクを描画します
getSeoMeta エントリーのSEOパネルの値と、テンプレートの代替値を解決します
getSiteSettings 公開用のサイトの識別情報と、そのほかの全体設定を読み込みます
getTaxonomyTerms タクソノミーのナビゲーション、絞り込み、タームの一覧を描画します
getTerm アーカイブページのために、タクソノミーのタームを1件読み込みます
getTermsForEntries エントリーの一覧について、1つのタクソノミーをまとめて読み込みます
search コレクションをまたいで、公開済みのコンテンツを検索します

コンテンツのクエリ

EmDashのクエリ関数は、Astroのライブコンテンツコレクションの方式に従い、エラーを穏当に扱えるように { entries, error } または { entry, error } を返します。

本サイトの補足 やさしい解説:やさしい解説

WordPressのテーマでは、テンプレートの中で記事を取り出して表示します。EmDashでは、その役割をAstroのページが担い、getEmDashCollection()(一覧)や getEmDashEntry()(1件)を呼び出してエントリーを受け取ります。これらの関数は、失敗しても例外を投げず、結果の中の error で知らせます。記事が見つからないことはエラーではなく、entrynull になるだけです。そのため、ページでは「error があればエラーを返す」「entry がなければ404ページに移動する」の2つを確認します。

getEmDashCollection()

コレクションのすべてのエントリーを取得します。次の例では、すべての投稿を読み込み、エラーを確認します。

import { getEmDashCollection } from "emdash";

const { entries: posts, error } = await getEmDashCollection("posts");

if (error) {
	console.error("Failed to load posts:", error);
}

引数

引数 説明
collection string コレクションのスラッグ
options CollectionFilter 任意の絞り込みのオプション

オプション

options 引数は、次の絞り込みを受け付けます。

interface WhereRange {
	gt?: string;
	gte?: string;
	lt?: string;
	lte?: string;
}

interface CollectionFilter {
	status?: "draft" | "published" | "archived";
	limit?: number;
	cursor?: string; // Keyset pagination — pass a previous `nextCursor`
	offset?: number; // Offset pagination — skip N entries (use with `limit`)
	where?: Record<string, string | string[] | WhereRange>;
	orderBy?: Record<string, "asc" | "desc">;
	locale?: string;
}

cursoroffset は同時に使えません。where のキーには、コンテンツのフィールド、タクソノミーbyline を指定できます。大小を比較する場合は、範囲のオブジェクトを使えます。

戻り値

この関数は、CollectionResult で解決されます。

interface CollectionResult<T> {
	entries: ContentEntry<T>[]; // Empty array if error or none found
	error?: Error; // Set if query failed
	cacheHint: CacheHint; // Tags and last-modified time for Astro route caching
	nextCursor?: string; // Cursor for the next keyset page, if any
	hasMore?: boolean; // Whether more entries exist beyond this page (when `limit` is set)
}

次の例では、ステータスとタクソノミーで絞り込み、件数を制限し、エラーを扱います。

// Get all published posts
const { entries: posts } = await getEmDashCollection("posts", {
	status: "published",
});

// Get latest 5 posts
const { entries: latest } = await getEmDashCollection("posts", {
	limit: 5,
	status: "published",
});

// Filter by taxonomy
const { entries: newsPosts } = await getEmDashCollection("posts", {
	status: "published",
	where: { category: "news" },
});

// Numbered archive page (e.g. /page/3) with offset pagination
const perPage = 20;
const page = Number(Astro.params.page ?? 1);
const { entries: pagePosts, hasMore } = await getEmDashCollection("posts", {
	status: "published",
	limit: perPage,
	offset: (page - 1) * perPage,
	orderBy: { published_at: "desc" },
});

// Handle errors
const { entries, error } = await getEmDashCollection("posts");
if (error) {
	return new Response("Server error", { status: 500 });
}

getEmDashEntry()

スラッグまたはIDで1件のエントリーを取得します。次の例では、投稿を読み込み、見つからない場合はリダイレクトします。

import { getEmDashEntry } from "emdash";

const { entry: post, error } = await getEmDashEntry("posts", "my-post-slug");

if (!post) {
	return Astro.redirect("/404");
}

引数

引数 説明
collection string コレクションのスラッグ
slugOrId string エントリーのスラッグまたはID
options { locale?: string } 任意。スラッグの解決に使うロケール

プレビューモードは自動的に扱われます。リクエストに有効な _preview トークンがある場合、クエリは下書きのコンテンツを返します。任意の options 引数が受け付けるのは、スラッグの解決に使う locale だけです。プレビューの状態に引数は必要ありません。

戻り値

この関数は、EntryResult で解決されます。

interface EntryResult<T> {
	entry: ContentEntry<T> | null; // null if not found
	error?: Error; // Set only for actual errors, not "not found"
	isPreview: boolean; // true if draft content is being served
	fallbackLocale?: string; // Set when locale fallback returned another locale
	cacheHint: CacheHint; // Tags and last-modified time for Astro route caching
}

次の例では、スラッグとIDで取得し、プレビューの状態を読み取り、エラーと「見つからない」を区別します。

// Get by slug
const { entry: post } = await getEmDashEntry("posts", "hello-world");

// Get by ID
const { entry: post } = await getEmDashEntry("posts", "01HXK5MZSN0FVXT2Q3KPRT9M7D");

// Preview is automatic — isPreview is true when a valid _preview token is present
const { entry, isPreview, error } = await getEmDashEntry("posts", slug);

// Handle errors vs not-found
if (error) {
	return new Response("Server error", { status: 500 });
}
if (!entry) {
	return Astro.redirect("/404");
}

getTranslations()

コレクションのスラッグとデータベースのIDを指定して、1件のエントリーについて利用できる翻訳を取得します。

import { getTranslations } from "emdash";

const { translations, error } = await getTranslations("posts", post.data.id);

結果には、共通の translationGrouptranslations の配列、任意の error が含まれます。それぞれの翻訳の概要には、idlocaleslugstatus が含まれます。

resolveEmDashPath()

公開用のパス名を、ルーティング対象のコレクションに設定したURLパターンと照らし合わせて解決します。

import { resolveEmDashPath } from "emdash";

const result = await resolveEmDashPath("/blog/hello-world");

if (result) {
	console.log(result.collection, result.entry.data.title);
}

結果には、一致した collectionentry、ルートの params が含まれます。設定したURLパターンのどれにも一致しない場合、この関数は null を返します。

getEditMeta()

Portable Textの値に付けられた、列挙されないビジュアル編集用のメタデータを読み取ります。

import { getEditMeta } from "emdash";

const meta = getEditMeta(post.data.content);

注釈の付いた値では { collection, id, field } を返し、注釈がない値では undefined を返します。

コンテンツの型

ContentEntry

クエリ関数は、次の形でエントリーを返します。

interface ContentEntry<T = Record<string, unknown>> {
	id: string;
	data: T;
	edit: EditProxy; // Visual editing annotations
}

edit プロキシーは、ビジュアル編集用の注釈を提供します。要素に展開すると、その場での編集が有効になります({...entry.edit.title})。編集モード以外では、何も出力しません。

data オブジェクトには、すべてのコンテンツのフィールドと、システムのフィールドが含まれます。

  • id:一意の識別子
  • slug:URLに使える識別子
  • status:"draft" | "published" | "archived"
  • createdAt:作成日時(Date
  • updatedAt:最終更新日時(Date
  • publishedAt:公開日時(Date)または null。コンテンツを非公開にしても保持されます
  • ほかに、コレクションのスキーマで定義したすべての独自のフィールド

URL用のヘルパー

decodeSlug()slugify()

動的ルートのパラメーターをコンテンツのクエリに渡す前に、decodeSlug() を使います。パラメーターがない場合は undefined を返し、それ以外の場合は decodeURIComponent() を適用します。パーセントエンコーディングが不正な場合は、例外が発生します。

function decodeSlug(raw: string | undefined): string | undefined;
import { decodeSlug, getEmDashEntry } from "emdash";

const slug = decodeSlug(Astro.params.slug);
const { entry } = slug ? await getEmDashEntry("posts", slug) : { entry: null };

slugify(value) は、テキストを小文字でハイフン区切りのスラッグに変換します。テンプレートでラベルからスラッグを組み立てる必要がある場合に使います。保存されたエントリーのスラッグは、すでにEmDashから受け取れます。

sanitizeHref()isSafeHref()

これらのヘルパーは、javascript: のような安全でないリンクのスキームを拒否します。isSafeHref(value) は真偽値を返します。sanitizeHref(value) は、安全な場合は元のURLを返し、値が空または安全でない場合は "#" を返します。

import { sanitizeHref } from "emdash";

const href = sanitizeHref(menuItem.url);

プレビューの仕組み

generatePreviewToken()

下書きのコンテンツのプレビュートークンを生成します。次の例では、1時間で期限切れになるトークンを作成します。

import { generatePreviewToken } from "emdash";

const token = await generatePreviewToken({
	contentId: "posts:01HXK5MZSN...",
	secret: process.env.EMDASH_PREVIEW_SECRET!,
	expiresIn: 3600, // 1 hour
});

contentIdcollection:id の形式にする必要があります。expiresIn には、秒数、または smhdw で終わる期間を指定でき、既定値は "1h" です。署名用の秘密鍵はサーバー側で保持します。

verifyPreviewToken()

プレビュートークンを検証し、そのペイロードを読み取ります。

import { verifyPreviewToken } from "emdash";

const result = await verifyPreviewToken({
	token,
	secret: process.env.EMDASH_PREVIEW_SECRET!,
});

if (result.valid) {
	const { cid, exp, iat } = result.payload;
	// cid is "collection:id" format, e.g. "posts:my-draft-post"
}

署名用の secret と一緒に、token または url のどちらかを渡します。トークンが無効な場合は { valid: false, error } を返します。error"none""malformed""invalid""expired" のいずれかです。

parseContentId()

プレビューのペイロードの collection:id の値を、2つの部分に分けます。

import { parseContentId } from "emdash";

const parsed = parseContentId(result.payload.cid);

{ collection, id } を返します。値に区切りのコロンがない場合は、例外が発生します。

getPreviewUrl()buildPreviewUrl()

getPreviewUrl() は、プレビューのURLを作成して署名します。collectionidsecret と、任意の expiresInbaseUrlpathPatternlocale を受け付けます。

import { getPreviewUrl } from "emdash";

const previewUrl = await getPreviewUrl({
	collection: "posts",
	id: post.id,
	secret: process.env.EMDASH_PREVIEW_SECRET!,
	pathPattern: "/blog/{id}",
});

baseUrl を指定しない場合は、サイト内の相対URLを返します。トークンがすでにある場合は、buildPreviewUrl({ path, token, baseUrl? }) を使います。

isPreviewRequest()

リクエストにプレビュートークンが含まれているかを確認し、トークンを読み取ります。

import { isPreviewRequest, getPreviewToken } from "emdash";

if (isPreviewRequest(Astro.url)) {
	const token = getPreviewToken(Astro.url);
	// Verify and show preview content
}

getPreviewToken() は、_preview クエリパラメーターを返し、ない場合は null を返します。通常のプレビューのリクエストはEmDashのミドルウェアが検証し、プレビューの状態を自動的に getEmDashEntry() に渡します。これらのヘルパーは、独自のプレビュー用のルートやツールのためのものです。

コンテンツの変換

Portable TextとProseMirrorの形式を相互に変換します。

import { prosemirrorToPortableText, portableTextToProsemirror } from "emdash";

// From ProseMirror (editor) to Portable Text (storage)
const portableText = prosemirrorToPortableText(prosemirrorDoc);

// From Portable Text to ProseMirror
const prosemirrorDoc = portableTextToProsemirror(portableText);

サイト設定

サイト全体の設定は、getSiteSettingsgetSiteSetting で読み込みます。

function getSiteSettings(): Promise<Partial<SiteSettings>>;
function getSiteSetting<K extends SiteSettingKey>(key: K): Promise<SiteSettings[K] | undefined>;
import { getSiteSettings, getSiteSetting } from "emdash";

// Get all settings
const settings = await getSiteSettings();

// Get single setting
const title = await getSiteSetting("title");

ランタイムのAPIでは、設定は読み取り専用です。更新するには管理用のAPIを使います。

getSiteSettings() は、設定されていないキーを省くため、一部の項目だけを持つオブジェクトを返します。サイトのロゴやファビコンのようなメディアの設定は、関数が値を返す前に、メディアの参照のオブジェクトに解決されます。

getSiteSettingsWithCacheHint(){ data, cacheHint } を返します。サイト設定が変わったあとにAstroのルートキャッシュを無効にする必要がある場合は、このヒントを Astro.cache.set() に渡します。

SEO

ページのSEOパネルの値とコンテンツからの代替値は、getSeoMeta() で解決します。

function getSeoMeta<T>(content: SeoContentInput<T>, options?: SeoMetaOptions): SeoMeta;

interface SeoMetaOptions {
	siteTitle?: string;
	siteUrl?: string;
	titleSeparator?: string; // Default: " | "
	path?: string;
	defaultOgImage?: string;
	defaultTitle?: string;
	defaultDescription?: string;
}

interface SeoMeta {
	title: string;
	description: string | null;
	ogTitle: string;
	ogDescription: string | null;
	ogImage: string | null;
	canonical: string | null;
	robots: string | null;
}
import { getSeoMeta } from "emdash";

const meta = getSeoMeta(post, {
	siteTitle: "Example Blog",
	siteUrl: "https://example.com",
	path: `/blog/${post.data.slug}`,
});

解決済みのタイトル、説明、Open Graphの値、正規URL(canonical)、robotsの値を返します。getContentSeo(content) は、テンプレートの代替値を適用せずに、そのままのSEOのオブジェクトを返します。

翻訳されたコンテンツでは、getHreflangAlternates(collection, entryId, { siteUrl? }) が、公開済みでルーティング対象のロケール別の版を { hreflang, href } のオブジェクトとして返し、x-default の項目を加えます。国際化が無効な場合、現在のエントリーに noindex が指定されている場合、サイトの絶対URLが得られない場合は、空の配列を返します。

コメント

エントリーの承認済みのコメントと、その件数を取得します。

import { getCommentCount, getComments } from "emdash";

const { items: comments, total } = await getComments({
	collection: "posts",
	contentId: post.data.id,
	threaded: true,
	reactions: true,
	sort: "best",
});

const count = await getCommentCount("posts", post.data.id);

threaded を指定すると、返信を親のコメントの下に入れ子にします。sort の既定値は "oldest" です。"best" は、最上位のコメントをリアクションの数で順位付けし、リアクションの件数を自動的に有効にします。サーバーで描画するコメントのクエリは、承認済みのコメントを最大500件まで返します。クライアント側でページ分割が必要な場合は、REST APIを使います。

ナビゲーションのメニューを取得し、入れ子になった子の項目を含めて、項目を順に処理します。

function getMenu(name: string, options?: { locale?: string }): Promise<Menu | null>;
function getMenus(options?: { locale?: string }): Promise<MenuSummary[]>;
import { getMenu, getMenus } from "emdash";

// Get all menus
const menus = await getMenus();

// Get specific menu with items
const primaryMenu = await getMenu("primary");

if (primaryMenu) {
	primaryMenu.items.forEach(item => {
		console.log(item.label, item.url);
		// Nested items for dropdowns
		item.children.forEach(child => console.log("  -", child.label));
	});
}

getMenu(name, { locale? }) は、設定したロケールの代替の順番に従います。getMenus({ locale? }) は、リクエストから解決したロケールまたは設定したロケールについて、メニューの概要を一覧にします。国際化を使っていない場合は、すべてのロケールを一覧にします。getMenuWithCacheHint() は、Astroのキャッシュを使うルートのために { data, cacheHint } を返します。

バイライン

IDまたはスラッグで執筆者のプロフィールを取得するか、バイラインにクレジットされたエントリーを一覧にします。

import { getByline, getBylineBySlug, getEntriesByByline } from "emdash";

const profile = await getBylineBySlug("jane-doe", { locale: "en" });
const posts = profile
	? await getEntriesByByline("posts", profile.translationGroup ?? profile.id)
	: [];

getByline(id) は、1件のプロフィールまたは null を返します。スラッグでの取得は任意のロケールを受け付け、ロケールの代替の順番に従います。コンテンツのクエリは、エントリーの順序付きのクレジットをすでに entry.data.bylines に入れています。これらの単独のヘルパーは、執筆者のページやバイラインのアーカイブに使います。

タクソノミー

タクソノミーのターム、1件のターム、エントリーのターム、タームに属するエントリーを取得します。

function getTaxonomyTerms(
	taxonomyName: string,
	options?: { locale?: string; includeCounts?: boolean },
): Promise<TaxonomyTerm[]>;

function getTerm(
	taxonomyName: string,
	slug: string,
	options?: { locale?: string; includeCounts?: boolean },
): Promise<TaxonomyTerm | null>;

function getTermsForEntries(
	collection: string,
	entryIds: string[],
	taxonomyName: string,
	options?: { locale?: string },
): Promise<Map<string, TaxonomyTerm[]>>;
import { getTaxonomyTerms, getTerm, getEntryTerms, getEntriesByTerm } from "emdash";

// Get all terms for a taxonomy (tree structure for hierarchical)
const categories = await getTaxonomyTerms("category");

// Get single term
const news = await getTerm("category", "news");

// Get terms assigned to a content entry
const postCategories = await getEntryTerms("posts", "post-123", "category");

// Get entries with a specific term
const newsPosts = await getEntriesByTerm("posts", "category", "news");

getTaxonomyDefs({ locale? }) はタクソノミーの定義を一覧にし、getTaxonomyDef(name, { locale? }) は1件の定義または null を返します。ロケールを考慮した定義とタームの取得は、設定した代替の順番に従います。

getTaxonomyTerms(name, { locale?, includeCounts? }) は、階層のあるタクソノミーではツリーを返し、既定では表示されるエントリーの件数を含めます。テンプレートで件数を描画しない場合は、includeCounts: false を渡します。コンテンツのクエリも、割り当てられたタームをそれぞれのエントリーの data.terms に入れます。コレクション名とエントリーのIDしか手元にない場合は、getEntryTerms() を使います。

多くのエントリーの横にタームを描画するアーカイブページでは、ループの中で getEntryTerms() を呼び出す代わりに、まとめて取得します。

import { getAllTermsForEntries, getTermsForEntries } from "emdash";

const termsByPost = await getTermsForEntries(
	"posts",
	posts.map(post => post.data.id),
	"category",
);

const allTermsByPost = await getAllTermsForEntries(
	"posts",
	posts.map(post => post.data.id),
);

getTermsForEntries() は、エントリーのIDから、指定したタクソノミーのタームへのマップを返します。getAllTermsForEntries() は、エントリーのIDから、タクソノミー名ごとにまとめたタームへのマップを返します。getTaxonomyTermsWithCacheHint() は、Astroでキャッシュするルートのために { data, cacheHint } を返します。

ウィジェットエリア

ウィジェットエリアと、そこに含まれるウィジェットを取得します。

import { getWidgetArea, getWidgetAreas } from "emdash";

// Get all widget areas
const areas = await getWidgetAreas();

// Get specific widget area with widgets
const sidebar = await getWidgetArea("sidebar");

if (sidebar) {
	sidebar.widgets.forEach(widget => {
		console.log(widget.type, widget.title);
	});
}

getWidgetAreaWithCacheHint(name) は、Astroのキャッシュを使うルートのために { data, cacheHint } を返します。ウィジェットエリアとそのウィジェットは、設定された並び順で並びます。

セクション

セクションを取得し、絞り込みます。

import { getSection, getSections } from "emdash";

// Get all sections (paginated)
const { items, nextCursor } = await getSections();

// Filter sections
const { items: themeSections } = await getSections({ source: "theme" });
const { items: results } = await getSections({ search: "newsletter" });

// Get a single section by slug
const cta = await getSection("newsletter-cta");

getSections(options?){ items: Section[]; nextCursor?: string } を返します。オプションは、source"theme" | "user" | "import")、searchlimit(既定値50、最大100)、cursor です。

コレクションをまたいで、サイト全体を検索します。結果には、該当箇所を強調した抜粋が含まれます。

function search(query: string, options?: SearchOptions): Promise<SearchResponse>;

interface SearchOptions {
	collections?: string[]; // Default: every searchable collection
	status?: string; // Default: "published"
	locale?: string; // Default: all locales
	limit?: number; // Default: 20
	cursor?: string;
}

interface SearchResponse {
	items: SearchResult[];
	nextCursor?: string;
}
import { search } from "emdash";

const results = await search("hello world", {
	collections: ["posts", "pages"],
	status: "published",
	limit: 20,
});

// search() resolves to { items, nextCursor? }
results.items.forEach(result => {
	console.log(result.title);
	console.log(result.snippet); // Contains <mark> tags
	console.log(result.score);
});

// Paginate: pass the previous nextCursor back as `cursor` to get the next page.
// nextCursor is undefined once there are no more results.
if (results.nextCursor) {
	const next = await search("hello world", {
		collections: ["posts", "pages"],
		limit: 20,
		cursor: results.nextCursor,
	});
}

エラーの扱い

コンテンツのクエリは、処理上のエラーを例外として投げずに、結果の中で返します。エントリーが見つからないことは処理上のエラーではありません。その場合、entrynull で、error は未定義のままです。

const { entry, error } = await getEmDashEntry("posts", slug);

if (error) {
	return new Response("Content could not be loaded", { status: 500 });
}

if (!entry) {
	return Astro.redirect("/404");
}

結果を包むオブジェクトを返さないヘルパーは、入力が不正な場合やデータベースの操作が失敗した場合に、例外を投げることがあります。ページで役に立つ代わりの表示を出せる場合は、ルートの境界でそれらのエラーを扱います。低レベルのサーバー統合が使うリポジトリやハンドラーのエラークラスは、このサイトテンプレートAPIの範囲外です。