← ドキュメント

ニュースやブログを管理する

コンテンツAPI ガイド

管理画面でコンテンツセットと記事を作成し、読み取り専用の API キーで React / Next.js や静的サイトから 取得する手順です。npm パッケージは不要で、標準の fetch だけで組み込めます。

このページで説明する公開 Content API(pcms_... キー、/api/public/... エンドポイント)は読み取り専用です。記事の作成・更新・削除はできません(記事の編集は管理画面でのみ行えます)。
記事の作成・更新を API や AI エージェント(Claude Code など)から行いたい場合は、書き込み対応の Agent API(/api/agent/...)をご利用ください。手順は Claude Code から記事を投稿 を参照してください。

API キーを入力して試す →

1. コンテンツを公開する

  1. 管理画面でサイトを作成し、サイドバーから対象サイトを選択します。
  2. 管理画面の /dashboard/settings でコンテンツセットを作成し、公開 に設定します。記事の並び順もここで設定できます(デフォルトは投稿日の新しい順)。
  3. /dashboard/content-sets/:slug で記事を作成し、ステータスを 公開 にします。掲載期間を指定した場合、その期間内だけ API に含まれます。
概念説明
site公開先サイトの単位。slug(例: your-site-slug)が公開 API のパスに使われます
content-setニュース・ブログなどの箱。slug はセット ID(例: news)です
entry記事1件。タイトル・slug・本文 HTML・サムネ・投稿日・タグなどを含みます
サイト slug はサイトごとに異なります。管理画面の /dashboard/settings で確認してください。アカウント作成時の最初のサイトは default になりがちですが、追加したサイトは別の slug です。API キーとパスの slug は同じサイトに揃えてください。ずれると 403 site_mismatch になります。

2. API キーを発行する

  1. 管理画面の /dashboard/settings で対象サイトを選択し、API キーを新規作成してラベルを付けます。
  2. 表示された pcms_... 形式のキーを控えます(再表示できません)。
ここで発行する API キーはそのサイトの公開コンテンツを読み取る権限だけを持ちます。別サイトのパスには使えません。作成・更新・削除はできません。
書き込み用キーは別物です(/api/agent/... 向け)。発行時に「AI(Claude Code など)から記事を投稿・更新を許可」を入れたキーを使い、詳細は Claude Code から記事を投稿 を参照してください。

3. エンドポイント

ベース URL: https://api.presto.pw
認証: Authorization: Bearer pcms_...
CORS: /api/public/* は任意オリジンから取得できます。

MethodPath用途
GET/api/public/sites/:siteSlug/content-sets指定サイトの公開中コンテンツセット一覧
GET/api/public/sites/:siteSlug/content-sets/:slug/entries公開中記事一覧(?limit=20 任意、デフォルト 50・最大 300)。 タグで絞り込む場合は ?tag=お知らせ(完全一致・40 文字まで)。 ページングは ?page= / ?offset=、無限スクロールは ?cursor=。並び順はコンテンツセットの entrySort 設定に従います
GET/api/public/sites/:siteSlug/content-sets/:slug/entries/:entryIdID で1件
GET/api/public/sites/:siteSlug/content-sets/:slug/entries/by-slug/:entrySlugslug で1件(詳細ページ向き)
GET/api/public/sites/:siteSlug/content-sets/:slug/tags指定コンテンツセットの公開中記事に付いたタグ一覧(重複除去・日本語順)
GET/api/media/:keyサムネ画像(認証不要。レスポンスの thumbnailUrl をそのまま使えます)
GET/api/og/:tenantId/sites/:siteSlug/content-sets/:slug/entries/:entryId.pngOGP 画像(認証不要。サムネ未設定時はタイトル入りカードを自動生成。設定済みならサムネへ 302 リダイレクト)

レスポンス例(コンテンツセット1件)

{
  "id": "news",
  "name": "ニュース",
  "color": "#1c1c1a",
  "entrySort": "postedAtDesc"
}

entrySort は記事一覧の並び順です。取りうる値は postedAtDesc(投稿日が新しい順)、postedAtAsc(投稿日が古い順)、updatedAtDesc(更新日が新しい順)、createdAtDesc(作成日が新しい順)です。管理画面の /dashboard/settings で変更できます。

レスポンス例(記事一覧)

{
  "items": [
    {
      "id": "c0h3abc123",
      "contentSetId": "news",
      "title": "リニューアルのお知らせ",
      "slug": "renewal",
      "bodyHtml": "<p>本文...</p>",
      "author": "編集部",
      "thumbnailUrl": "https://api.presto.pw/api/media/tenantId/xxx.jpg",
      "ogImageUrl": "https://api.presto.pw/api/media/tenantId/xxx.jpg",
      "postedAt": "2026-07-01T00:00:00.000Z",
      "updatedAt": "2026-07-02T00:00:00.000Z",
      "tags": ["お知らせ", "リニューアル"]
    }
  ],
  "limit": 10,
  "hasMore": true,
  "nextCursor": "eyJ2IjoxLCJzIjoicG9zdGVkQXREZXNjIiw...",
  "page": 1,
  "offset": 0,
  "total": 42
}

items に記事配列が入ります。hasMoretrue のとき続きがあります。無限スクロールでは nextCursor ?cursor= に付けて次を取得してください。ページ番号方式では ?page=2 または ?offset=10 を使えます。

cursorpage / offset は同時に指定できません。 カーソルはコンテンツセットの並び順(entrySort)が変わると無効になります。

レスポンス例(記事1件)

{
  "id": "c0h3abc123",
  "contentSetId": "news",
  "title": "リニューアルのお知らせ",
  "slug": "renewal",
  "bodyHtml": "<p>本文...</p>",
  "author": "編集部",
  "thumbnailUrl": "https://api.presto.pw/api/media/tenantId/xxx.jpg",
  "ogImageUrl": "https://api.presto.pw/api/media/tenantId/xxx.jpg",
  "postedAt": "2026-07-01T00:00:00.000Z",
  "updatedAt": "2026-07-02T00:00:00.000Z",
  "tags": ["お知らせ", "リニューアル"]
}

内部向けフィールド(status・Lexical JSON の body など)は含まれません。 本文は保存時に HTML 化された bodyHtml を返します。

レスポンス例(タグ一覧)

{
  "tags": ["お知らせ", "リニューアル"]
}

指定コンテンツセット内の公開中かつ掲載期間内の記事からタグを集約します。 他のコンテンツセットのタグは含まれません。

ogImageUrl は SNS 共有用の OGP 画像 URL です。サムネイルがある記事では thumbnailUrl と同じ URL になり、未設定の場合は presto がタイトル入りの 1200×630 PNG を自動生成します。顧客サイトの og:image には常に ogImageUrl を使ってください。

4. curl で取得する

# コンテンツセット一覧(your-site-slug は管理画面のサイト slug に置き換え)
curl -H "Authorization: Bearer pcms_YOUR_KEY" \
  "https://api.presto.pw/api/public/sites/your-site-slug/content-sets"

# ニュース記事を10件(並び順はコンテンツセットの entrySort 設定に従う)
curl -H "Authorization: Bearer pcms_YOUR_KEY" \
  "https://api.presto.pw/api/public/sites/your-site-slug/content-sets/news/entries?limit=10"

# 2ページ目(page 方式)
curl -H "Authorization: Bearer pcms_YOUR_KEY" \
  "https://api.presto.pw/api/public/sites/your-site-slug/content-sets/news/entries?limit=10&page=2"

# 続きを取得(cursor 方式・無限スクロール向き)
curl -H "Authorization: Bearer pcms_YOUR_KEY" \
  "https://api.presto.pw/api/public/sites/your-site-slug/content-sets/news/entries?limit=10&cursor=eyJ2Ijox..."

# slug で1件
curl -H "Authorization: Bearer pcms_YOUR_KEY" \
  "https://api.presto.pw/api/public/sites/your-site-slug/content-sets/news/entries/by-slug/renewal"

# タグ一覧(news コンテンツセット内の公開中記事のみ)
curl -H "Authorization: Bearer pcms_YOUR_KEY" \
  "https://api.presto.pw/api/public/sites/your-site-slug/content-sets/news/tags"

5. React / Next.js から組み込む

追加パッケージは不要です。環境変数やサーバー側で API キーを渡し、fetch Authorization ヘッダーに付与してください。

API キーをブラウザに露出すると、誰でも読み取り専用でコンテンツを取得できます。Next.js では Route Handler や Server Component 側でキーを付与する構成を推奨します。

一覧ページ(ニュース10件)

const API_BASE = 'https://api.presto.pw'
const API_KEY = process.env.PRESTO_API_KEY // サーバー側で設定
const SITE_SLUG = process.env.PRESTO_SITE_SLUG // 管理画面で確認したサイト slug

type PublicEntry = {
  id: string
  contentSetId: string
  title: string
  slug: string | null
  bodyHtml: string
  author: string
  thumbnailUrl: string | null
  ogImageUrl: string
  postedAt: string | null
  updatedAt: string
  tags: string[]
}

export async function fetchNewsEntries(siteSlug: string): Promise<PublicEntry[]> {
  const res = await fetch(
    `${API_BASE}/api/public/sites/${siteSlug}/content-sets/news/entries?limit=10`,
    { headers: { Authorization: `Bearer ${API_KEY}` }, next: { revalidate: 60 } },
  )
  if (!res.ok) throw new Error('failed to fetch entries')
  const { items } = await res.json()
  return items
}

// 使用例: const entries = await fetchNewsEntries(SITE_SLUG)

export function NewsList({ entries }: { entries: PublicEntry[] }) {
  return (
    <ul>
      {entries.map((entry) => (
        <li key={entry.id}>
          {entry.thumbnailUrl && (
            <img src={entry.thumbnailUrl} alt="" width={120} height={80} />
          )}
          <a href={`/news/${entry.slug ?? entry.id}`}>{entry.title}</a>
          {entry.postedAt && <time dateTime={entry.postedAt}>{entry.postedAt.slice(0, 10)}</time>}
        </li>
      ))}
    </ul>
  )
}

詳細ページ(slug で1件)

export async function fetchNewsEntry(entrySlug: string, siteSlug: string): Promise<PublicEntry | null> {
  const res = await fetch(
    `${API_BASE}/api/public/sites/${siteSlug}/content-sets/news/entries/by-slug/${entrySlug}`,
    { headers: { Authorization: `Bearer ${API_KEY}` }, next: { revalidate: 60 } },
  )
  if (res.status === 404) return null
  if (!res.ok) throw new Error('failed to fetch entry')
  return res.json()
}

export function NewsArticle({ entry }: { entry: PublicEntry }) {
  return (
    <article>
      <h1>{entry.title}</h1>
      {entry.thumbnailUrl && <img src={entry.thumbnailUrl} alt="" />}
      {/* bodyHtml は自テナント管理下の HTML です */}
      <div dangerouslySetInnerHTML={{ __html: entry.bodyHtml }} />
    </article>
  )
}

// Next.js App Router: 記事詳細の metadata
export async function generateMetadata({ params }: { params: { slug: string } }) {
  const entry = await fetchNewsEntry(params.slug, SITE_SLUG)
  if (!entry) return {}

  return {
    title: entry.title,
    openGraph: {
      title: entry.title,
      type: 'article',
      images: [{ url: entry.ogImageUrl, width: 1200, height: 630 }],
    },
    twitter: {
      card: 'summary_large_image',
      title: entry.title,
      images: [entry.ogImageUrl],
    },
  }
}

静的 HTML で OGP を埋め込む場合

<head>
  <meta property="og:title" content="リニューアルのお知らせ" />
  <meta property="og:type" content="article" />
  <meta property="og:image" content="https://api.presto.pw/api/og/1/sites/your-site-slug/content-sets/news/entries/c0h3abc123.png" />
  <meta name="twitter:card" content="summary_large_image" />
  <meta name="twitter:image" content="https://api.presto.pw/api/og/1/sites/your-site-slug/content-sets/news/entries/c0h3abc123.png" />
</head>

ビルド時に API から記事を取得し、上記の og:image にはレスポンスの ogImageUrl をそのまま入れてください。OGP 画像 URL は API キー不要なので、SNS クローラが直接取得できます。

無限スクロール(cursor 方式)

type EntryListResponse = {
  items: PublicEntry[]
  limit: number
  hasMore: boolean
  nextCursor: string | null
  page: number | null
  offset: number | null
  total: number
}

export async function fetchNewsEntriesPage(
  siteSlug: string,
  cursor?: string,
): Promise<EntryListResponse> {
  const params = new URLSearchParams({ limit: '10' })
  if (cursor) params.set('cursor', cursor)
  const res = await fetch(
    `${API_BASE}/api/public/sites/${siteSlug}/content-sets/news/entries?${params}`,
    { headers: { Authorization: `Bearer ${API_KEY}` } },
  )
  if (!res.ok) throw new Error('failed to fetch entries')
  return res.json()
}

// 使用例: 初回取得後、hasMore なら nextCursor で続きを取得
const first = await fetchNewsEntriesPage(SITE_SLUG)
if (first.hasMore && first.nextCursor) {
  const second = await fetchNewsEntriesPage(SITE_SLUG, first.nextCursor)
  console.log([...first.items, ...second.items])
}

クライアントコンポーネントから使う場合

useEffect(() => {
  fetch(`${API_BASE}/api/public/sites/${process.env.NEXT_PUBLIC_PRESTO_SITE_SLUG}/content-sets/news/entries?limit=5`, {
    headers: { Authorization: `Bearer ${process.env.NEXT_PUBLIC_PRESTO_API_KEY}` },
  })
    .then((res) => res.json())
    .then((data) => setEntries(data.items))
    .catch(console.error)
}, [])

6. 画像と本文

サムネイルはレスポンスの thumbnailUrl(絶対 URL)をそのまま <img src> に使えます。OGP 共有には ogImageUrl を使い、サムネ未設定時は presto が記事タイトル・コンテンツセット名・サイト名入りの画像を自動生成します。本文は bodyHtml を HTML として表示してください(React では dangerouslySetInnerHTML)。